Tooltips.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. import {
  2. isString,
  3. assign,
  4. forEach
  5. } from 'min-dash';
  6. import {
  7. assignStyle,
  8. domify,
  9. attr as domAttr,
  10. classes as domClasses,
  11. remove as domRemove,
  12. delegate as domDelegate
  13. } from 'min-dom';
  14. import Ids from '../../util/IdGenerator';
  15. // document wide unique tooltip ids
  16. var ids = new Ids('tt');
  17. function createRoot(parentNode) {
  18. var root = domify(
  19. '<div class="djs-tooltip-container" />'
  20. );
  21. assignStyle(root, {
  22. position: 'absolute',
  23. width: '0',
  24. height: '0'
  25. });
  26. parentNode.insertBefore(root, parentNode.firstChild);
  27. return root;
  28. }
  29. function setPosition(el, x, y) {
  30. assignStyle(el, { left: x + 'px', top: y + 'px' });
  31. }
  32. function setVisible(el, visible) {
  33. el.style.display = visible === false ? 'none' : '';
  34. }
  35. var tooltipClass = 'djs-tooltip',
  36. tooltipSelector = '.' + tooltipClass;
  37. /**
  38. * A service that allows users to render tool tips on the diagram.
  39. *
  40. * The tooltip service will take care of updating the tooltip positioning
  41. * during navigation + zooming.
  42. *
  43. * @example
  44. *
  45. * ```javascript
  46. *
  47. * // add a pink badge on the top left of the shape
  48. * tooltips.add({
  49. * position: {
  50. * x: 50,
  51. * y: 100
  52. * },
  53. * html: '<div style="width: 10px; background: fuchsia; color: white;">0</div>'
  54. * });
  55. *
  56. * // or with optional life span
  57. * tooltips.add({
  58. * position: {
  59. * top: -5,
  60. * left: -5
  61. * },
  62. * html: '<div style="width: 10px; background: fuchsia; color: white;">0</div>',
  63. * ttl: 2000
  64. * });
  65. *
  66. * // remove a tool tip
  67. * var id = tooltips.add(...);
  68. * tooltips.remove(id);
  69. * ```
  70. *
  71. * @param {EventBus} eventBus
  72. * @param {Canvas} canvas
  73. */
  74. export default function Tooltips(eventBus, canvas) {
  75. this._eventBus = eventBus;
  76. this._canvas = canvas;
  77. this._ids = ids;
  78. this._tooltipDefaults = {
  79. show: {
  80. minZoom: 0.7,
  81. maxZoom: 5.0
  82. }
  83. };
  84. /**
  85. * Mapping tooltipId -> tooltip
  86. */
  87. this._tooltips = {};
  88. // root html element for all tooltips
  89. this._tooltipRoot = createRoot(canvas.getContainer());
  90. var self = this;
  91. domDelegate.bind(this._tooltipRoot, tooltipSelector, 'mousedown', function(event) {
  92. event.stopPropagation();
  93. });
  94. domDelegate.bind(this._tooltipRoot, tooltipSelector, 'mouseover', function(event) {
  95. self.trigger('mouseover', event);
  96. });
  97. domDelegate.bind(this._tooltipRoot, tooltipSelector, 'mouseout', function(event) {
  98. self.trigger('mouseout', event);
  99. });
  100. this._init();
  101. }
  102. Tooltips.$inject = [ 'eventBus', 'canvas' ];
  103. /**
  104. * Adds a HTML tooltip to the diagram
  105. *
  106. * @param {Object} tooltip the tooltip configuration
  107. *
  108. * @param {string|DOMElement} tooltip.html html element to use as an tooltip
  109. * @param {Object} [tooltip.show] show configuration
  110. * @param {number} [tooltip.show.minZoom] minimal zoom level to show the tooltip
  111. * @param {number} [tooltip.show.maxZoom] maximum zoom level to show the tooltip
  112. * @param {Object} tooltip.position where to attach the tooltip
  113. * @param {number} [tooltip.position.left] relative to element bbox left attachment
  114. * @param {number} [tooltip.position.top] relative to element bbox top attachment
  115. * @param {number} [tooltip.position.bottom] relative to element bbox bottom attachment
  116. * @param {number} [tooltip.position.right] relative to element bbox right attachment
  117. * @param {number} [tooltip.timeout=-1]
  118. *
  119. * @return {string} id that may be used to reference the tooltip for update or removal
  120. */
  121. Tooltips.prototype.add = function(tooltip) {
  122. if (!tooltip.position) {
  123. throw new Error('must specifiy tooltip position');
  124. }
  125. if (!tooltip.html) {
  126. throw new Error('must specifiy tooltip html');
  127. }
  128. var id = this._ids.next();
  129. tooltip = assign({}, this._tooltipDefaults, tooltip, {
  130. id: id
  131. });
  132. this._addTooltip(tooltip);
  133. if (tooltip.timeout) {
  134. this.setTimeout(tooltip);
  135. }
  136. return id;
  137. };
  138. Tooltips.prototype.trigger = function(action, event) {
  139. var node = event.delegateTarget || event.target;
  140. var tooltip = this.get(domAttr(node, 'data-tooltip-id'));
  141. if (!tooltip) {
  142. return;
  143. }
  144. if (action === 'mouseover' && tooltip.timeout) {
  145. this.clearTimeout(tooltip);
  146. }
  147. if (action === 'mouseout' && tooltip.timeout) {
  148. // cut timeout after mouse out
  149. tooltip.timeout = 1000;
  150. this.setTimeout(tooltip);
  151. }
  152. };
  153. /**
  154. * Get a tooltip with the given id
  155. *
  156. * @param {string} id
  157. */
  158. Tooltips.prototype.get = function(id) {
  159. if (typeof id !== 'string') {
  160. id = id.id;
  161. }
  162. return this._tooltips[id];
  163. };
  164. Tooltips.prototype.clearTimeout = function(tooltip) {
  165. tooltip = this.get(tooltip);
  166. if (!tooltip) {
  167. return;
  168. }
  169. var removeTimer = tooltip.removeTimer;
  170. if (removeTimer) {
  171. clearTimeout(removeTimer);
  172. tooltip.removeTimer = null;
  173. }
  174. };
  175. Tooltips.prototype.setTimeout = function(tooltip) {
  176. tooltip = this.get(tooltip);
  177. if (!tooltip) {
  178. return;
  179. }
  180. this.clearTimeout(tooltip);
  181. var self = this;
  182. tooltip.removeTimer = setTimeout(function() {
  183. self.remove(tooltip);
  184. }, tooltip.timeout);
  185. };
  186. /**
  187. * Remove an tooltip with the given id
  188. *
  189. * @param {string} id
  190. */
  191. Tooltips.prototype.remove = function(id) {
  192. var tooltip = this.get(id);
  193. if (tooltip) {
  194. domRemove(tooltip.html);
  195. domRemove(tooltip.htmlContainer);
  196. delete tooltip.htmlContainer;
  197. delete this._tooltips[tooltip.id];
  198. }
  199. };
  200. Tooltips.prototype.show = function() {
  201. setVisible(this._tooltipRoot);
  202. };
  203. Tooltips.prototype.hide = function() {
  204. setVisible(this._tooltipRoot, false);
  205. };
  206. Tooltips.prototype._updateRoot = function(viewbox) {
  207. var a = viewbox.scale || 1;
  208. var d = viewbox.scale || 1;
  209. var matrix = 'matrix(' + a + ',0,0,' + d + ',' + (-1 * viewbox.x * a) + ',' + (-1 * viewbox.y * d) + ')';
  210. this._tooltipRoot.style.transform = matrix;
  211. this._tooltipRoot.style['-ms-transform'] = matrix;
  212. };
  213. Tooltips.prototype._addTooltip = function(tooltip) {
  214. var id = tooltip.id,
  215. html = tooltip.html,
  216. htmlContainer,
  217. tooltipRoot = this._tooltipRoot;
  218. // unwrap jquery (for those who need it)
  219. if (html.get && html.constructor.prototype.jquery) {
  220. html = html.get(0);
  221. }
  222. // create proper html elements from
  223. // tooltip HTML strings
  224. if (isString(html)) {
  225. html = domify(html);
  226. }
  227. htmlContainer = domify('<div data-tooltip-id="' + id + '" class="' + tooltipClass + '">');
  228. assignStyle(htmlContainer, { position: 'absolute' });
  229. htmlContainer.appendChild(html);
  230. if (tooltip.type) {
  231. domClasses(htmlContainer).add('djs-tooltip-' + tooltip.type);
  232. }
  233. if (tooltip.className) {
  234. domClasses(htmlContainer).add(tooltip.className);
  235. }
  236. tooltip.htmlContainer = htmlContainer;
  237. tooltipRoot.appendChild(htmlContainer);
  238. this._tooltips[id] = tooltip;
  239. this._updateTooltip(tooltip);
  240. };
  241. Tooltips.prototype._updateTooltip = function(tooltip) {
  242. var position = tooltip.position,
  243. htmlContainer = tooltip.htmlContainer;
  244. // update overlay html based on tooltip x, y
  245. setPosition(htmlContainer, position.x, position.y);
  246. };
  247. Tooltips.prototype._updateTooltipVisibilty = function(viewbox) {
  248. forEach(this._tooltips, function(tooltip) {
  249. var show = tooltip.show,
  250. htmlContainer = tooltip.htmlContainer,
  251. visible = true;
  252. if (show) {
  253. if (show.minZoom > viewbox.scale ||
  254. show.maxZoom < viewbox.scale) {
  255. visible = false;
  256. }
  257. setVisible(htmlContainer, visible);
  258. }
  259. });
  260. };
  261. Tooltips.prototype._init = function() {
  262. var self = this;
  263. // scroll/zoom integration
  264. function updateViewbox(viewbox) {
  265. self._updateRoot(viewbox);
  266. self._updateTooltipVisibilty(viewbox);
  267. self.show();
  268. }
  269. this._eventBus.on('canvas.viewbox.changing', function(event) {
  270. self.hide();
  271. });
  272. this._eventBus.on('canvas.viewbox.changed', function(event) {
  273. updateViewbox(event.viewbox);
  274. });
  275. };