Tooltips.js 7.8 KB

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