diagram-js
Version:
A toolbox for displaying and modifying diagrams on the web
396 lines (293 loc) • 7.57 kB
JavaScript
import {
isString,
assign,
forEach
} from 'min-dash';
import {
assignStyle,
domify,
attr as domAttr,
classes as domClasses,
remove as domRemove,
delegate as domDelegate
} from 'min-dom';
import Ids from '../../util/IdGenerator';
/**
* @typedef {import('../../core/Canvas').default} Canvas
* @typedef {import('../../core/EventBus').default} EventBus
*
* @typedef {import('../../util/Types').RectTRBL} RectTRBL
*
* @typedef { {
* html: string | HTMLElement;
* position: RectTRBL;
* show?: {
* minZoom?: number;
* maxZoom?: number;
* };
* timeout?: number;
* } } Tooltip
*/
// document wide unique tooltip ids
var ids = new Ids('tt');
function createRoot(parentNode) {
var root = domify(
'<div class="djs-tooltip-container" />'
);
assignStyle(root, {
position: 'absolute',
width: '0',
height: '0'
});
parentNode.insertBefore(root, parentNode.firstChild);
return root;
}
function setPosition(el, x, y) {
assignStyle(el, { left: x + 'px', top: y + 'px' });
}
function setVisible(el, visible) {
el.style.display = visible === false ? 'none' : '';
}
var tooltipClass = 'djs-tooltip',
tooltipSelector = '.' + tooltipClass;
/**
* A service that allows users to render tool tips on the diagram.
*
* The tooltip service will take care of updating the tooltip positioning
* during navigation + zooming.
*
* @example
*
* ```javascript
*
* // add a pink badge on the top left of the shape
* tooltips.add({
* position: {
* x: 50,
* y: 100
* },
* html: '<div style="width: 10px; background: fuchsia; color: white;">0</div>'
* });
*
* // or with optional life span
* tooltips.add({
* position: {
* top: -5,
* left: -5
* },
* html: '<div style="width: 10px; background: fuchsia; color: white;">0</div>',
* ttl: 2000
* });
*
* // remove a tool tip
* var id = tooltips.add(...);
*
* tooltips.remove(id);
* ```
*
* @param {EventBus} eventBus
* @param {Canvas} canvas
*/
export default function Tooltips(eventBus, canvas) {
this._eventBus = eventBus;
this._canvas = canvas;
this._ids = ids;
this._tooltipDefaults = {
show: {
minZoom: 0.7,
maxZoom: 5.0
}
};
/**
* @type {Record<string, Tooltip>}
*/
this._tooltips = {};
// root html element for all tooltips
this._tooltipRoot = createRoot(canvas.getContainer());
var self = this;
domDelegate.bind(this._tooltipRoot, tooltipSelector, 'mousedown', function(event) {
event.stopPropagation();
});
domDelegate.bind(this._tooltipRoot, tooltipSelector, 'mouseover', function(event) {
self.trigger('mouseover', event);
});
domDelegate.bind(this._tooltipRoot, tooltipSelector, 'mouseout', function(event) {
self.trigger('mouseout', event);
});
this._init();
}
Tooltips.$inject = [ 'eventBus', 'canvas' ];
/**
* Adds an HTML tooltip to the diagram.
*
* @param {Tooltip} tooltip
*
* @return {string} ID of the tooltip.
*/
Tooltips.prototype.add = function(tooltip) {
if (!tooltip.position) {
throw new Error('must specifiy tooltip position');
}
if (!tooltip.html) {
throw new Error('must specifiy tooltip html');
}
var id = this._ids.next();
tooltip = assign({}, this._tooltipDefaults, tooltip, {
id: id
});
this._addTooltip(tooltip);
if (tooltip.timeout) {
this.setTimeout(tooltip);
}
return id;
};
/**
* @param {string} action
* @param {Event} event
*/
Tooltips.prototype.trigger = function(action, event) {
var node = event.delegateTarget || event.target;
var tooltip = this.get(domAttr(node, 'data-tooltip-id'));
if (!tooltip) {
return;
}
if (action === 'mouseover' && tooltip.timeout) {
this.clearTimeout(tooltip);
}
if (action === 'mouseout' && tooltip.timeout) {
// cut timeout after mouse out
tooltip.timeout = 1000;
this.setTimeout(tooltip);
}
};
/**
* Get tooltip with given ID.
*
* @param {Tooltip|string} id
*
* @return {Tooltip|undefined}
*/
Tooltips.prototype.get = function(id) {
if (typeof id !== 'string') {
id = id.id;
}
return this._tooltips[id];
};
/**
* @param {Tooltip} tooltip
*/
Tooltips.prototype.clearTimeout = function(tooltip) {
tooltip = this.get(tooltip);
if (!tooltip) {
return;
}
var removeTimer = tooltip.removeTimer;
if (removeTimer) {
clearTimeout(removeTimer);
tooltip.removeTimer = null;
}
};
/**
* @param {Tooltip} tooltip
*/
Tooltips.prototype.setTimeout = function(tooltip) {
tooltip = this.get(tooltip);
if (!tooltip) {
return;
}
this.clearTimeout(tooltip);
var self = this;
tooltip.removeTimer = setTimeout(function() {
self.remove(tooltip);
}, tooltip.timeout);
};
/**
* Remove tooltip with given ID.
*
* @param {string | Tooltip} id
*/
Tooltips.prototype.remove = function(id) {
var tooltip = this.get(id);
if (tooltip) {
domRemove(tooltip.html);
domRemove(tooltip.htmlContainer);
delete tooltip.htmlContainer;
delete this._tooltips[tooltip.id];
}
};
Tooltips.prototype.show = function() {
setVisible(this._tooltipRoot);
};
Tooltips.prototype.hide = function() {
setVisible(this._tooltipRoot, false);
};
Tooltips.prototype._updateRoot = function(viewbox) {
var a = viewbox.scale || 1;
var d = viewbox.scale || 1;
var matrix = 'matrix(' + a + ',0,0,' + d + ',' + (-1 * viewbox.x * a) + ',' + (-1 * viewbox.y * d) + ')';
this._tooltipRoot.style.transform = matrix;
this._tooltipRoot.style['-ms-transform'] = matrix;
};
Tooltips.prototype._addTooltip = function(tooltip) {
var id = tooltip.id,
html = tooltip.html,
htmlContainer,
tooltipRoot = this._tooltipRoot;
// unwrap jquery (for those who need it)
if (html.get && html.constructor.prototype.jquery) {
html = html.get(0);
}
// create proper html elements from
// tooltip HTML strings
if (isString(html)) {
html = domify(html);
}
htmlContainer = domify('<div data-tooltip-id="' + id + '" class="' + tooltipClass + '">');
assignStyle(htmlContainer, { position: 'absolute' });
htmlContainer.appendChild(html);
if (tooltip.type) {
domClasses(htmlContainer).add('djs-tooltip-' + tooltip.type);
}
if (tooltip.className) {
domClasses(htmlContainer).add(tooltip.className);
}
tooltip.htmlContainer = htmlContainer;
tooltipRoot.appendChild(htmlContainer);
this._tooltips[id] = tooltip;
this._updateTooltip(tooltip);
};
Tooltips.prototype._updateTooltip = function(tooltip) {
var position = tooltip.position,
htmlContainer = tooltip.htmlContainer;
// update overlay html based on tooltip x, y
setPosition(htmlContainer, position.x, position.y);
};
Tooltips.prototype._updateTooltipVisibilty = function(viewbox) {
forEach(this._tooltips, function(tooltip) {
var show = tooltip.show,
htmlContainer = tooltip.htmlContainer,
visible = true;
if (show) {
if (show.minZoom > viewbox.scale ||
show.maxZoom < viewbox.scale) {
visible = false;
}
setVisible(htmlContainer, visible);
}
});
};
Tooltips.prototype._init = function() {
var self = this;
// scroll/zoom integration
function updateViewbox(viewbox) {
self._updateRoot(viewbox);
self._updateTooltipVisibilty(viewbox);
self.show();
}
this._eventBus.on('canvas.viewbox.changing', function(event) {
self.hide();
});
this._eventBus.on('canvas.viewbox.changed', function(event) {
updateViewbox(event.viewbox);
});
};