ContextPad.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import {
  2. assign,
  3. isFunction,
  4. isArray,
  5. forEach,
  6. isDefined
  7. } from 'min-dash';
  8. import {
  9. delegate as domDelegate,
  10. event as domEvent,
  11. attr as domAttr,
  12. query as domQuery,
  13. classes as domClasses,
  14. domify as domify
  15. } from 'min-dom';
  16. var entrySelector = '.entry';
  17. /**
  18. * A context pad that displays element specific, contextual actions next
  19. * to a diagram element.
  20. *
  21. * @param {Object} config
  22. * @param {Boolean|Object} [config.scale={ min: 1.0, max: 1.5 }]
  23. * @param {Number} [config.scale.min]
  24. * @param {Number} [config.scale.max]
  25. * @param {EventBus} eventBus
  26. * @param {Overlays} overlays
  27. */
  28. export default function ContextPad(config, eventBus, overlays) {
  29. this._providers = [];
  30. this._eventBus = eventBus;
  31. this._overlays = overlays;
  32. var scale = isDefined(config && config.scale) ? config.scale : {
  33. min: 1,
  34. max: 1.5
  35. };
  36. this._overlaysConfig = {
  37. position: {
  38. right: -9,
  39. top: -6
  40. },
  41. scale: scale
  42. };
  43. this._current = null;
  44. this._init();
  45. }
  46. ContextPad.$inject = [
  47. 'config.contextPad',
  48. 'eventBus',
  49. 'overlays'
  50. ];
  51. /**
  52. * Registers events needed for interaction with other components
  53. */
  54. ContextPad.prototype._init = function() {
  55. var eventBus = this._eventBus;
  56. var self = this;
  57. eventBus.on('selection.changed', function(e) {
  58. var selection = e.newSelection;
  59. if (selection.length === 1) {
  60. self.open(selection[0]);
  61. } else {
  62. self.close();
  63. }
  64. });
  65. eventBus.on('elements.delete', function(event) {
  66. var elements = event.elements;
  67. forEach(elements, function(e) {
  68. if (self.isOpen(e)) {
  69. self.close();
  70. }
  71. });
  72. });
  73. eventBus.on('element.changed', function(event) {
  74. var element = event.element,
  75. current = self._current;
  76. // force reopen if element for which we are currently opened changed
  77. if (current && current.element === element) {
  78. self.open(element, true);
  79. }
  80. });
  81. };
  82. /**
  83. * Register a provider with the context pad
  84. *
  85. * @param {ContextPadProvider} provider
  86. */
  87. ContextPad.prototype.registerProvider = function(provider) {
  88. this._providers.push(provider);
  89. };
  90. /**
  91. * Returns the context pad entries for a given element
  92. *
  93. * @param {djs.element.Base} element
  94. *
  95. * @return {Array<ContextPadEntryDescriptor>} list of entries
  96. */
  97. ContextPad.prototype.getEntries = function(element) {
  98. var entries = {};
  99. // loop through all providers and their entries.
  100. // group entries by id so that overriding an entry is possible
  101. forEach(this._providers, function(provider) {
  102. var e = provider.getContextPadEntries(element);
  103. forEach(e, function(entry, id) {
  104. entries[id] = entry;
  105. });
  106. });
  107. return entries;
  108. };
  109. /**
  110. * Trigger an action available on the opened context pad
  111. *
  112. * @param {String} action
  113. * @param {Event} event
  114. * @param {Boolean} [autoActivate=false]
  115. */
  116. ContextPad.prototype.trigger = function(action, event, autoActivate) {
  117. var element = this._current.element,
  118. entries = this._current.entries,
  119. entry,
  120. handler,
  121. originalEvent,
  122. button = event.delegateTarget || event.target;
  123. if (!button) {
  124. return event.preventDefault();
  125. }
  126. entry = entries[domAttr(button, 'data-action')];
  127. handler = entry.action;
  128. originalEvent = event.originalEvent || event;
  129. // simple action (via callback function)
  130. if (isFunction(handler)) {
  131. if (action === 'click') {
  132. return handler(originalEvent, element, autoActivate);
  133. }
  134. } else {
  135. if (handler[action]) {
  136. return handler[action](originalEvent, element, autoActivate);
  137. }
  138. }
  139. // silence other actions
  140. event.preventDefault();
  141. };
  142. /**
  143. * Open the context pad for the given element
  144. *
  145. * @param {djs.model.Base} element
  146. * @param {Boolean} force if true, force reopening the context pad
  147. */
  148. ContextPad.prototype.open = function(element, force) {
  149. if (!force && this.isOpen(element)) {
  150. return;
  151. }
  152. this.close();
  153. this._updateAndOpen(element);
  154. };
  155. ContextPad.prototype._updateAndOpen = function(element) {
  156. var entries = this.getEntries(element),
  157. pad = this.getPad(element),
  158. html = pad.html;
  159. forEach(entries, function(entry, id) {
  160. var grouping = entry.group || 'default',
  161. control = domify(entry.html || '<div class="entry" draggable="true"></div>'),
  162. container;
  163. domAttr(control, 'data-action', id);
  164. container = domQuery('[data-group=' + grouping + ']', html);
  165. if (!container) {
  166. container = domify('<div class="group" data-group="' + grouping + '"></div>');
  167. html.appendChild(container);
  168. }
  169. container.appendChild(control);
  170. if (entry.className) {
  171. addClasses(control, entry.className);
  172. }
  173. if (entry.title) {
  174. domAttr(control, 'title', entry.title);
  175. }
  176. if (entry.imageUrl) {
  177. control.appendChild(domify('<img src="' + entry.imageUrl + '">'));
  178. }
  179. });
  180. domClasses(html).add('open');
  181. this._current = {
  182. element: element,
  183. pad: pad,
  184. entries: entries
  185. };
  186. this._eventBus.fire('contextPad.open', { current: this._current });
  187. };
  188. ContextPad.prototype.getPad = function(element) {
  189. if (this.isOpen()) {
  190. return this._current.pad;
  191. }
  192. var self = this;
  193. var overlays = this._overlays;
  194. var html = domify('<div class="djs-context-pad"></div>');
  195. var overlaysConfig = assign({
  196. html: html
  197. }, this._overlaysConfig);
  198. domDelegate.bind(html, entrySelector, 'click', function(event) {
  199. self.trigger('click', event);
  200. });
  201. domDelegate.bind(html, entrySelector, 'dragstart', function(event) {
  202. self.trigger('dragstart', event);
  203. });
  204. // stop propagation of mouse events
  205. domEvent.bind(html, 'mousedown', function(event) {
  206. event.stopPropagation();
  207. });
  208. this._overlayId = overlays.add(element, 'context-pad', overlaysConfig);
  209. var pad = overlays.get(this._overlayId);
  210. this._eventBus.fire('contextPad.create', { element: element, pad: pad });
  211. return pad;
  212. };
  213. /**
  214. * Close the context pad
  215. */
  216. ContextPad.prototype.close = function() {
  217. if (!this.isOpen()) {
  218. return;
  219. }
  220. this._overlays.remove(this._overlayId);
  221. this._overlayId = null;
  222. this._eventBus.fire('contextPad.close', { current: this._current });
  223. this._current = null;
  224. };
  225. /**
  226. * Check if pad is open. If element is given, will check
  227. * if pad is opened with given element.
  228. *
  229. * @param {Element} element
  230. * @return {Boolean}
  231. */
  232. ContextPad.prototype.isOpen = function(element) {
  233. return !!this._current && (!element ? true : this._current.element === element);
  234. };
  235. // helpers //////////////////////
  236. function addClasses(element, classNames) {
  237. var classes = domClasses(element);
  238. var actualClassNames = isArray(classNames) ? classNames : classNames.split(/\s+/g);
  239. actualClassNames.forEach(function(cls) {
  240. classes.add(cls);
  241. });
  242. }