Palette.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import {
  2. isFunction,
  3. isArray,
  4. forEach
  5. } from 'min-dash';
  6. import {
  7. domify,
  8. query as domQuery,
  9. attr as domAttr,
  10. clear as domClear,
  11. classes as domClasses,
  12. matches as domMatches,
  13. delegate as domDelegate,
  14. event as domEvent
  15. } from 'min-dom';
  16. var TOGGLE_SELECTOR = '.djs-palette-toggle',
  17. ENTRY_SELECTOR = '.entry',
  18. ELEMENT_SELECTOR = TOGGLE_SELECTOR + ', ' + ENTRY_SELECTOR;
  19. var PALETTE_OPEN_CLS = 'open',
  20. PALETTE_TWO_COLUMN_CLS = 'two-column';
  21. /**
  22. * A palette containing modeling elements.
  23. */
  24. export default function Palette(eventBus, canvas) {
  25. this._eventBus = eventBus;
  26. this._canvas = canvas;
  27. this._providers = [];
  28. var self = this;
  29. eventBus.on('tool-manager.update', function(event) {
  30. var tool = event.tool;
  31. self.updateToolHighlight(tool);
  32. });
  33. eventBus.on('i18n.changed', function() {
  34. self._update();
  35. });
  36. eventBus.on('diagram.init', function() {
  37. self._diagramInitialized = true;
  38. // initialize + update once diagram is ready
  39. if (self._providers.length) {
  40. self._init();
  41. self._update();
  42. }
  43. });
  44. }
  45. Palette.$inject = [ 'eventBus', 'canvas' ];
  46. /**
  47. * Register a provider with the palette
  48. *
  49. * @param {PaletteProvider} provider
  50. */
  51. Palette.prototype.registerProvider = function(provider) {
  52. this._providers.push(provider);
  53. // postpone init / update until diagram is initialized
  54. if (!this._diagramInitialized) {
  55. return;
  56. }
  57. if (!this._container) {
  58. this._init();
  59. }
  60. this._update();
  61. };
  62. /**
  63. * Returns the palette entries for a given element
  64. *
  65. * @return {Array<PaletteEntryDescriptor>} list of entries
  66. */
  67. Palette.prototype.getEntries = function() {
  68. var entries = {};
  69. // loop through all providers and their entries.
  70. // group entries by id so that overriding an entry is possible
  71. forEach(this._providers, function(provider) {
  72. var e = provider.getPaletteEntries();
  73. forEach(e, function(entry, id) {
  74. entries[id] = entry;
  75. });
  76. });
  77. return entries;
  78. };
  79. /**
  80. * Initialize
  81. */
  82. Palette.prototype._init = function() {
  83. var canvas = this._canvas,
  84. eventBus = this._eventBus;
  85. var parent = canvas.getContainer(),
  86. container = this._container = domify(Palette.HTML_MARKUP),
  87. self = this;
  88. parent.appendChild(container);
  89. domDelegate.bind(container, ELEMENT_SELECTOR, 'click', function(event) {
  90. var target = event.delegateTarget;
  91. if (domMatches(target, TOGGLE_SELECTOR)) {
  92. return self.toggle();
  93. }
  94. self.trigger('click', event);
  95. });
  96. // prevent drag propagation
  97. domEvent.bind(container, 'mousedown', function(event) {
  98. event.stopPropagation();
  99. });
  100. // prevent drag propagation
  101. domDelegate.bind(container, ENTRY_SELECTOR, 'dragstart', function(event) {
  102. self.trigger('dragstart', event);
  103. });
  104. eventBus.on('canvas.resized', this._layoutChanged, this);
  105. eventBus.fire('palette.create', {
  106. container: container
  107. });
  108. };
  109. /**
  110. * Update palette state.
  111. *
  112. * @param {Object} [state] { open, twoColumn }
  113. */
  114. Palette.prototype._toggleState = function(state) {
  115. state = state || {};
  116. var parent = this._getParentContainer(),
  117. container = this._container;
  118. var eventBus = this._eventBus;
  119. var twoColumn;
  120. var cls = domClasses(container);
  121. if ('twoColumn' in state) {
  122. twoColumn = state.twoColumn;
  123. } else {
  124. twoColumn = this._needsCollapse(parent.clientHeight, this._entries || {});
  125. }
  126. // always update two column
  127. cls.toggle(PALETTE_TWO_COLUMN_CLS, twoColumn);
  128. if ('open' in state) {
  129. cls.toggle(PALETTE_OPEN_CLS, state.open);
  130. }
  131. eventBus.fire('palette.changed', {
  132. twoColumn: twoColumn,
  133. open: this.isOpen()
  134. });
  135. };
  136. Palette.prototype._update = function() {
  137. var entriesContainer = domQuery('.djs-palette-entries', this._container),
  138. entries = this._entries = this.getEntries();
  139. domClear(entriesContainer);
  140. forEach(entries, function(entry, id) {
  141. var grouping = entry.group || 'default';
  142. var container = domQuery('[data-group=' + grouping + ']', entriesContainer);
  143. if (!container) {
  144. container = domify('<div class="group" data-group="' + grouping + '"></div>');
  145. entriesContainer.appendChild(container);
  146. }
  147. var html = entry.html || (
  148. entry.separator ?
  149. '<hr class="separator" />' :
  150. '<div class="entry" draggable="true"></div>');
  151. var control = domify(html);
  152. container.appendChild(control);
  153. if (!entry.separator) {
  154. domAttr(control, 'data-action', id);
  155. if (entry.title) {
  156. domAttr(control, 'title', entry.title);
  157. }
  158. if (entry.className) {
  159. addClasses(control, entry.className);
  160. }
  161. if (entry.imageUrl) {
  162. control.appendChild(domify('<img src="' + entry.imageUrl + '">'));
  163. }
  164. }
  165. });
  166. // open after update
  167. this.open();
  168. };
  169. /**
  170. * Trigger an action available on the palette
  171. *
  172. * @param {String} action
  173. * @param {Event} event
  174. */
  175. Palette.prototype.trigger = function(action, event, autoActivate) {
  176. var entries = this._entries,
  177. entry,
  178. handler,
  179. originalEvent,
  180. button = event.delegateTarget || event.target;
  181. if (!button) {
  182. return event.preventDefault();
  183. }
  184. entry = entries[domAttr(button, 'data-action')];
  185. // when user clicks on the palette and not on an action
  186. if (!entry) {
  187. return;
  188. }
  189. handler = entry.action;
  190. originalEvent = event.originalEvent || event;
  191. // simple action (via callback function)
  192. if (isFunction(handler)) {
  193. if (action === 'click') {
  194. handler(originalEvent, autoActivate);
  195. }
  196. } else {
  197. if (handler[action]) {
  198. handler[action](originalEvent, autoActivate);
  199. }
  200. }
  201. // silence other actions
  202. event.preventDefault();
  203. };
  204. Palette.prototype._layoutChanged = function() {
  205. this._toggleState({});
  206. };
  207. /**
  208. * Do we need to collapse to two columns?
  209. *
  210. * @param {Number} availableHeight
  211. * @param {Object} entries
  212. *
  213. * @return {Boolean}
  214. */
  215. Palette.prototype._needsCollapse = function(availableHeight, entries) {
  216. // top margin + bottom toggle + bottom margin
  217. // implementors must override this method if they
  218. // change the palette styles
  219. var margin = 20 + 10 + 20;
  220. var entriesHeight = Object.keys(entries).length * 46;
  221. return availableHeight < entriesHeight + margin;
  222. };
  223. /**
  224. * Close the palette
  225. */
  226. Palette.prototype.close = function() {
  227. this._toggleState({
  228. open: false,
  229. twoColumn: false
  230. });
  231. };
  232. /**
  233. * Open the palette
  234. */
  235. Palette.prototype.open = function() {
  236. this._toggleState({ open: true });
  237. };
  238. Palette.prototype.toggle = function(open) {
  239. if (this.isOpen()) {
  240. this.close();
  241. } else {
  242. this.open();
  243. }
  244. };
  245. Palette.prototype.isActiveTool = function(tool) {
  246. return tool && this._activeTool === tool;
  247. };
  248. Palette.prototype.updateToolHighlight = function(name) {
  249. var entriesContainer,
  250. toolsContainer;
  251. if (!this._toolsContainer) {
  252. entriesContainer = domQuery('.djs-palette-entries', this._container);
  253. this._toolsContainer = domQuery('[data-group=tools]', entriesContainer);
  254. }
  255. toolsContainer = this._toolsContainer;
  256. forEach(toolsContainer.children, function(tool) {
  257. var actionName = tool.getAttribute('data-action');
  258. if (!actionName) {
  259. return;
  260. }
  261. var toolClasses = domClasses(tool);
  262. actionName = actionName.replace('-tool', '');
  263. if (toolClasses.contains('entry') && actionName === name) {
  264. toolClasses.add('highlighted-entry');
  265. } else {
  266. toolClasses.remove('highlighted-entry');
  267. }
  268. });
  269. };
  270. /**
  271. * Return true if the palette is opened.
  272. *
  273. * @example
  274. *
  275. * palette.open();
  276. *
  277. * if (palette.isOpen()) {
  278. * // yes, we are open
  279. * }
  280. *
  281. * @return {boolean} true if palette is opened
  282. */
  283. Palette.prototype.isOpen = function() {
  284. return domClasses(this._container).has(PALETTE_OPEN_CLS);
  285. };
  286. /**
  287. * Get container the palette lives in.
  288. *
  289. * @return {Element}
  290. */
  291. Palette.prototype._getParentContainer = function() {
  292. return this._canvas.getContainer();
  293. };
  294. /* markup definition */
  295. Palette.HTML_MARKUP =
  296. '<div class="djs-palette">' +
  297. '<div class="djs-palette-entries"></div>' +
  298. '<div class="djs-palette-toggle"></div>' +
  299. '</div>';
  300. // helpers //////////////////////
  301. function addClasses(element, classNames) {
  302. var classes = domClasses(element);
  303. var actualClassNames = isArray(classNames) ? classNames : classNames.split(/\s+/g);
  304. actualClassNames.forEach(function(cls) {
  305. classes.add(cls);
  306. });
  307. }