Overlays.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. import {
  2. isArray,
  3. isString,
  4. isObject,
  5. assign,
  6. forEach,
  7. find,
  8. filter,
  9. matchPattern,
  10. isDefined
  11. } from 'min-dash';
  12. import {
  13. domify,
  14. classes as domClasses,
  15. attr as domAttr,
  16. remove as domRemove,
  17. clear as domClear
  18. } from 'min-dom';
  19. import {
  20. getBBox
  21. } from '../../util/Elements';
  22. import Ids from '../../util/IdGenerator';
  23. // document wide unique overlay ids
  24. var ids = new Ids('ov');
  25. var LOW_PRIORITY = 500;
  26. /**
  27. * A service that allows users to attach overlays to diagram elements.
  28. *
  29. * The overlay service will take care of overlay positioning during updates.
  30. *
  31. * @example
  32. *
  33. * // add a pink badge on the top left of the shape
  34. * overlays.add(someShape, {
  35. * position: {
  36. * top: -5,
  37. * left: -5
  38. * },
  39. * html: '<div style="width: 10px; background: fuchsia; color: white;">0</div>'
  40. * });
  41. *
  42. * // or add via shape id
  43. *
  44. * overlays.add('some-element-id', {
  45. * position: {
  46. * top: -5,
  47. * left: -5
  48. * }
  49. * html: '<div style="width: 10px; background: fuchsia; color: white;">0</div>'
  50. * });
  51. *
  52. * // or add with optional type
  53. *
  54. * overlays.add(someShape, 'badge', {
  55. * position: {
  56. * top: -5,
  57. * left: -5
  58. * }
  59. * html: '<div style="width: 10px; background: fuchsia; color: white;">0</div>'
  60. * });
  61. *
  62. *
  63. * // remove an overlay
  64. *
  65. * var id = overlays.add(...);
  66. * overlays.remove(id);
  67. *
  68. *
  69. * You may configure overlay defaults during tool by providing a `config` module
  70. * with `overlays.defaults` as an entry:
  71. *
  72. * {
  73. * overlays: {
  74. * defaults: {
  75. * show: {
  76. * minZoom: 0.7,
  77. * maxZoom: 5.0
  78. * },
  79. * scale: {
  80. * min: 1
  81. * }
  82. * }
  83. * }
  84. *
  85. * @param {Object} config
  86. * @param {EventBus} eventBus
  87. * @param {Canvas} canvas
  88. * @param {ElementRegistry} elementRegistry
  89. */
  90. export default function Overlays(config, eventBus, canvas, elementRegistry) {
  91. this._eventBus = eventBus;
  92. this._canvas = canvas;
  93. this._elementRegistry = elementRegistry;
  94. this._ids = ids;
  95. this._overlayDefaults = assign({
  96. // no show constraints
  97. show: null,
  98. // always scale
  99. scale: true
  100. }, config && config.defaults);
  101. /**
  102. * Mapping overlayId -> overlay
  103. */
  104. this._overlays = {};
  105. /**
  106. * Mapping elementId -> overlay container
  107. */
  108. this._overlayContainers = [];
  109. // root html element for all overlays
  110. this._overlayRoot = createRoot(canvas.getContainer());
  111. this._init();
  112. }
  113. Overlays.$inject = [
  114. 'config.overlays',
  115. 'eventBus',
  116. 'canvas',
  117. 'elementRegistry'
  118. ];
  119. /**
  120. * Returns the overlay with the specified id or a list of overlays
  121. * for an element with a given type.
  122. *
  123. * @example
  124. *
  125. * // return the single overlay with the given id
  126. * overlays.get('some-id');
  127. *
  128. * // return all overlays for the shape
  129. * overlays.get({ element: someShape });
  130. *
  131. * // return all overlays on shape with type 'badge'
  132. * overlays.get({ element: someShape, type: 'badge' });
  133. *
  134. * // shape can also be specified as id
  135. * overlays.get({ element: 'element-id', type: 'badge' });
  136. *
  137. *
  138. * @param {Object} search
  139. * @param {String} [search.id]
  140. * @param {String|djs.model.Base} [search.element]
  141. * @param {String} [search.type]
  142. *
  143. * @return {Object|Array<Object>} the overlay(s)
  144. */
  145. Overlays.prototype.get = function(search) {
  146. if (isString(search)) {
  147. search = { id: search };
  148. }
  149. if (isString(search.element)) {
  150. search.element = this._elementRegistry.get(search.element);
  151. }
  152. if (search.element) {
  153. var container = this._getOverlayContainer(search.element, true);
  154. // return a list of overlays when searching by element (+type)
  155. if (container) {
  156. return search.type ? filter(container.overlays, matchPattern({ type: search.type })) : container.overlays.slice();
  157. } else {
  158. return [];
  159. }
  160. } else
  161. if (search.type) {
  162. return filter(this._overlays, matchPattern({ type: search.type }));
  163. } else {
  164. // return single element when searching by id
  165. return search.id ? this._overlays[search.id] : null;
  166. }
  167. };
  168. /**
  169. * Adds a HTML overlay to an element.
  170. *
  171. * @param {String|djs.model.Base} element attach overlay to this shape
  172. * @param {String} [type] optional type to assign to the overlay
  173. * @param {Object} overlay the overlay configuration
  174. *
  175. * @param {String|DOMElement} overlay.html html element to use as an overlay
  176. * @param {Object} [overlay.show] show configuration
  177. * @param {Number} [overlay.show.minZoom] minimal zoom level to show the overlay
  178. * @param {Number} [overlay.show.maxZoom] maximum zoom level to show the overlay
  179. * @param {Object} overlay.position where to attach the overlay
  180. * @param {Number} [overlay.position.left] relative to element bbox left attachment
  181. * @param {Number} [overlay.position.top] relative to element bbox top attachment
  182. * @param {Number} [overlay.position.bottom] relative to element bbox bottom attachment
  183. * @param {Number} [overlay.position.right] relative to element bbox right attachment
  184. * @param {Boolean|Object} [overlay.scale=true] false to preserve the same size regardless of
  185. * diagram zoom
  186. * @param {Number} [overlay.scale.min]
  187. * @param {Number} [overlay.scale.max]
  188. *
  189. * @return {String} id that may be used to reference the overlay for update or removal
  190. */
  191. Overlays.prototype.add = function(element, type, overlay) {
  192. if (isObject(type)) {
  193. overlay = type;
  194. type = null;
  195. }
  196. if (!element.id) {
  197. element = this._elementRegistry.get(element);
  198. }
  199. if (!overlay.position) {
  200. throw new Error('must specifiy overlay position');
  201. }
  202. if (!overlay.html) {
  203. throw new Error('must specifiy overlay html');
  204. }
  205. if (!element) {
  206. throw new Error('invalid element specified');
  207. }
  208. var id = this._ids.next();
  209. overlay = assign({}, this._overlayDefaults, overlay, {
  210. id: id,
  211. type: type,
  212. element: element,
  213. html: overlay.html
  214. });
  215. this._addOverlay(overlay);
  216. return id;
  217. };
  218. /**
  219. * Remove an overlay with the given id or all overlays matching the given filter.
  220. *
  221. * @see Overlays#get for filter options.
  222. *
  223. * @param {String} [id]
  224. * @param {Object} [filter]
  225. */
  226. Overlays.prototype.remove = function(filter) {
  227. var overlays = this.get(filter) || [];
  228. if (!isArray(overlays)) {
  229. overlays = [ overlays ];
  230. }
  231. var self = this;
  232. forEach(overlays, function(overlay) {
  233. var container = self._getOverlayContainer(overlay.element, true);
  234. if (overlay) {
  235. domRemove(overlay.html);
  236. domRemove(overlay.htmlContainer);
  237. delete overlay.htmlContainer;
  238. delete overlay.element;
  239. delete self._overlays[overlay.id];
  240. }
  241. if (container) {
  242. var idx = container.overlays.indexOf(overlay);
  243. if (idx !== -1) {
  244. container.overlays.splice(idx, 1);
  245. }
  246. }
  247. });
  248. };
  249. Overlays.prototype.show = function() {
  250. setVisible(this._overlayRoot);
  251. };
  252. Overlays.prototype.hide = function() {
  253. setVisible(this._overlayRoot, false);
  254. };
  255. Overlays.prototype.clear = function() {
  256. this._overlays = {};
  257. this._overlayContainers = [];
  258. domClear(this._overlayRoot);
  259. };
  260. Overlays.prototype._updateOverlayContainer = function(container) {
  261. var element = container.element,
  262. html = container.html;
  263. // update container left,top according to the elements x,y coordinates
  264. // this ensures we can attach child elements relative to this container
  265. var x = element.x,
  266. y = element.y;
  267. if (element.waypoints) {
  268. var bbox = getBBox(element);
  269. x = bbox.x;
  270. y = bbox.y;
  271. }
  272. setPosition(html, x, y);
  273. domAttr(container.html, 'data-container-id', element.id);
  274. };
  275. Overlays.prototype._updateOverlay = function(overlay) {
  276. var position = overlay.position,
  277. htmlContainer = overlay.htmlContainer,
  278. element = overlay.element;
  279. // update overlay html relative to shape because
  280. // it is already positioned on the element
  281. // update relative
  282. var left = position.left,
  283. top = position.top;
  284. if (position.right !== undefined) {
  285. var width;
  286. if (element.waypoints) {
  287. width = getBBox(element).width;
  288. } else {
  289. width = element.width;
  290. }
  291. left = position.right * -1 + width;
  292. }
  293. if (position.bottom !== undefined) {
  294. var height;
  295. if (element.waypoints) {
  296. height = getBBox(element).height;
  297. } else {
  298. height = element.height;
  299. }
  300. top = position.bottom * -1 + height;
  301. }
  302. setPosition(htmlContainer, left || 0, top || 0);
  303. };
  304. Overlays.prototype._createOverlayContainer = function(element) {
  305. var html = domify('<div class="djs-overlays" style="position: absolute" />');
  306. this._overlayRoot.appendChild(html);
  307. var container = {
  308. html: html,
  309. element: element,
  310. overlays: []
  311. };
  312. this._updateOverlayContainer(container);
  313. this._overlayContainers.push(container);
  314. return container;
  315. };
  316. Overlays.prototype._updateRoot = function(viewbox) {
  317. var scale = viewbox.scale || 1;
  318. var matrix = 'matrix(' +
  319. [
  320. scale,
  321. 0,
  322. 0,
  323. scale,
  324. -1 * viewbox.x * scale,
  325. -1 * viewbox.y * scale
  326. ].join(',') +
  327. ')';
  328. setTransform(this._overlayRoot, matrix);
  329. };
  330. Overlays.prototype._getOverlayContainer = function(element, raw) {
  331. var container = find(this._overlayContainers, function(c) {
  332. return c.element === element;
  333. });
  334. if (!container && !raw) {
  335. return this._createOverlayContainer(element);
  336. }
  337. return container;
  338. };
  339. Overlays.prototype._addOverlay = function(overlay) {
  340. var id = overlay.id,
  341. element = overlay.element,
  342. html = overlay.html,
  343. htmlContainer,
  344. overlayContainer;
  345. // unwrap jquery (for those who need it)
  346. if (html.get && html.constructor.prototype.jquery) {
  347. html = html.get(0);
  348. }
  349. // create proper html elements from
  350. // overlay HTML strings
  351. if (isString(html)) {
  352. html = domify(html);
  353. }
  354. overlayContainer = this._getOverlayContainer(element);
  355. htmlContainer = domify('<div class="djs-overlay" data-overlay-id="' + id + '" style="position: absolute">');
  356. htmlContainer.appendChild(html);
  357. if (overlay.type) {
  358. domClasses(htmlContainer).add('djs-overlay-' + overlay.type);
  359. }
  360. overlay.htmlContainer = htmlContainer;
  361. overlayContainer.overlays.push(overlay);
  362. overlayContainer.html.appendChild(htmlContainer);
  363. this._overlays[id] = overlay;
  364. this._updateOverlay(overlay);
  365. this._updateOverlayVisibilty(overlay, this._canvas.viewbox());
  366. };
  367. Overlays.prototype._updateOverlayVisibilty = function(overlay, viewbox) {
  368. var show = overlay.show,
  369. minZoom = show && show.minZoom,
  370. maxZoom = show && show.maxZoom,
  371. htmlContainer = overlay.htmlContainer,
  372. visible = true;
  373. if (show) {
  374. if (
  375. (isDefined(minZoom) && minZoom > viewbox.scale) ||
  376. (isDefined(maxZoom) && maxZoom < viewbox.scale)
  377. ) {
  378. visible = false;
  379. }
  380. setVisible(htmlContainer, visible);
  381. }
  382. this._updateOverlayScale(overlay, viewbox);
  383. };
  384. Overlays.prototype._updateOverlayScale = function(overlay, viewbox) {
  385. var shouldScale = overlay.scale,
  386. minScale,
  387. maxScale,
  388. htmlContainer = overlay.htmlContainer;
  389. var scale, transform = '';
  390. if (shouldScale !== true) {
  391. if (shouldScale === false) {
  392. minScale = 1;
  393. maxScale = 1;
  394. } else {
  395. minScale = shouldScale.min;
  396. maxScale = shouldScale.max;
  397. }
  398. if (isDefined(minScale) && viewbox.scale < minScale) {
  399. scale = (1 / viewbox.scale || 1) * minScale;
  400. }
  401. if (isDefined(maxScale) && viewbox.scale > maxScale) {
  402. scale = (1 / viewbox.scale || 1) * maxScale;
  403. }
  404. }
  405. if (isDefined(scale)) {
  406. transform = 'scale(' + scale + ',' + scale + ')';
  407. }
  408. setTransform(htmlContainer, transform);
  409. };
  410. Overlays.prototype._updateOverlaysVisibilty = function(viewbox) {
  411. var self = this;
  412. forEach(this._overlays, function(overlay) {
  413. self._updateOverlayVisibilty(overlay, viewbox);
  414. });
  415. };
  416. Overlays.prototype._init = function() {
  417. var eventBus = this._eventBus;
  418. var self = this;
  419. // scroll/zoom integration
  420. function updateViewbox(viewbox) {
  421. self._updateRoot(viewbox);
  422. self._updateOverlaysVisibilty(viewbox);
  423. self.show();
  424. }
  425. eventBus.on('canvas.viewbox.changing', function(event) {
  426. self.hide();
  427. });
  428. eventBus.on('canvas.viewbox.changed', function(event) {
  429. updateViewbox(event.viewbox);
  430. });
  431. // remove integration
  432. eventBus.on([ 'shape.remove', 'connection.remove' ], function(e) {
  433. var element = e.element;
  434. var overlays = self.get({ element: element });
  435. forEach(overlays, function(o) {
  436. self.remove(o.id);
  437. });
  438. var container = self._getOverlayContainer(element);
  439. if (container) {
  440. domRemove(container.html);
  441. var i = self._overlayContainers.indexOf(container);
  442. if (i !== -1) {
  443. self._overlayContainers.splice(i, 1);
  444. }
  445. }
  446. });
  447. // move integration
  448. eventBus.on('element.changed', LOW_PRIORITY, function(e) {
  449. var element = e.element;
  450. var container = self._getOverlayContainer(element, true);
  451. if (container) {
  452. forEach(container.overlays, function(overlay) {
  453. self._updateOverlay(overlay);
  454. });
  455. self._updateOverlayContainer(container);
  456. }
  457. });
  458. // marker integration, simply add them on the overlays as classes, too.
  459. eventBus.on('element.marker.update', function(e) {
  460. var container = self._getOverlayContainer(e.element, true);
  461. if (container) {
  462. domClasses(container.html)[e.add ? 'add' : 'remove'](e.marker);
  463. }
  464. });
  465. // clear overlays with diagram
  466. eventBus.on('diagram.clear', this.clear, this);
  467. };
  468. // helpers /////////////////////////////
  469. function createRoot(parentNode) {
  470. var root = domify(
  471. '<div class="djs-overlay-container" style="position: absolute; width: 0; height: 0;" />'
  472. );
  473. parentNode.insertBefore(root, parentNode.firstChild);
  474. return root;
  475. }
  476. function setPosition(el, x, y) {
  477. assign(el.style, { left: x + 'px', top: y + 'px' });
  478. }
  479. function setVisible(el, visible) {
  480. el.style.display = visible === false ? 'none' : '';
  481. }
  482. function setTransform(el, transform) {
  483. el.style['transform-origin'] = 'top left';
  484. [ '', '-ms-', '-webkit-' ].forEach(function(prefix) {
  485. el.style[prefix + 'transform'] = transform;
  486. });
  487. }