Overlays.js 15 KB

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