Palette.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. import {
  2. isArray,
  3. isFunction,
  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. import {
  17. escapeCSS
  18. } from '../../util/EscapeUtil';
  19. var TOGGLE_SELECTOR = '.djs-palette-toggle',
  20. ENTRY_SELECTOR = '.entry',
  21. ELEMENT_SELECTOR = TOGGLE_SELECTOR + ', ' + ENTRY_SELECTOR;
  22. var PALETTE_PREFIX = 'djs-palette-',
  23. PALETTE_SHOWN_CLS = 'shown',
  24. PALETTE_OPEN_CLS = 'open',
  25. PALETTE_TWO_COLUMN_CLS = 'two-column';
  26. var DEFAULT_PRIORITY = 1000;
  27. /**
  28. * A palette containing modeling elements.
  29. */
  30. export default function Palette(eventBus, canvas) {
  31. this._eventBus = eventBus;
  32. this._canvas = canvas;
  33. var self = this;
  34. eventBus.on('tool-manager.update', function(event) {
  35. var tool = event.tool;
  36. self.updateToolHighlight(tool);
  37. });
  38. eventBus.on('i18n.changed', function() {
  39. self._update();
  40. });
  41. eventBus.on('diagram.init', function() {
  42. self._diagramInitialized = true;
  43. self._rebuild();
  44. });
  45. }
  46. Palette.$inject = [ 'eventBus', 'canvas' ];
  47. /**
  48. * Register a provider with the palette
  49. *
  50. * @param {number} [priority=1000]
  51. * @param {PaletteProvider} provider
  52. *
  53. * @example
  54. * const paletteProvider = {
  55. * getPaletteEntries: function() {
  56. * return function(entries) {
  57. * return {
  58. * ...entries,
  59. * 'entry-1': {
  60. * label: 'My Entry',
  61. * action: function() { alert("I have been clicked!"); }
  62. * }
  63. * };
  64. * }
  65. * }
  66. * };
  67. *
  68. * palette.registerProvider(800, paletteProvider);
  69. */
  70. Palette.prototype.registerProvider = function(priority, provider) {
  71. if (!provider) {
  72. provider = priority;
  73. priority = DEFAULT_PRIORITY;
  74. }
  75. this._eventBus.on('palette.getProviders', priority, function(event) {
  76. event.providers.push(provider);
  77. });
  78. this._rebuild();
  79. };
  80. /**
  81. * Returns the palette entries
  82. *
  83. * @return {Object<string, PaletteEntryDescriptor>} map of entries
  84. */
  85. Palette.prototype.getEntries = function() {
  86. var providers = this._getProviders();
  87. return providers.reduce(addPaletteEntries, {});
  88. };
  89. Palette.prototype._rebuild = function() {
  90. if (!this._diagramInitialized) {
  91. return;
  92. }
  93. var providers = this._getProviders();
  94. if (!providers.length) {
  95. return;
  96. }
  97. if (!this._container) {
  98. this._init();
  99. }
  100. this._update();
  101. };
  102. /**
  103. * Initialize
  104. */
  105. Palette.prototype._init = function() {
  106. var self = this;
  107. var eventBus = this._eventBus;
  108. var parentContainer = this._getParentContainer();
  109. var container = this._container = domify(Palette.HTML_MARKUP);
  110. parentContainer.appendChild(container);
  111. domClasses(parentContainer).add(PALETTE_PREFIX + PALETTE_SHOWN_CLS);
  112. domDelegate.bind(container, ELEMENT_SELECTOR, 'click', function(event) {
  113. var target = event.delegateTarget;
  114. if (domMatches(target, TOGGLE_SELECTOR)) {
  115. return self.toggle();
  116. }
  117. self.trigger('click', event);
  118. });
  119. // prevent drag propagation
  120. domEvent.bind(container, 'mousedown', function(event) {
  121. event.stopPropagation();
  122. });
  123. // prevent drag propagation
  124. domDelegate.bind(container, ENTRY_SELECTOR, 'dragstart', function(event) {
  125. self.trigger('dragstart', event);
  126. });
  127. eventBus.on('canvas.resized', this._layoutChanged, this);
  128. eventBus.fire('palette.create', {
  129. container: container
  130. });
  131. };
  132. Palette.prototype._getProviders = function(id) {
  133. var event = this._eventBus.createEvent({
  134. type: 'palette.getProviders',
  135. providers: []
  136. });
  137. this._eventBus.fire(event);
  138. return event.providers;
  139. };
  140. /**
  141. * Update palette state.
  142. *
  143. * @param {Object} [state] { open, twoColumn }
  144. */
  145. Palette.prototype._toggleState = function(state) {
  146. state = state || {};
  147. var parent = this._getParentContainer(),
  148. container = this._container;
  149. var eventBus = this._eventBus;
  150. var twoColumn;
  151. var cls = domClasses(container),
  152. parentCls = domClasses(parent);
  153. if ('twoColumn' in state) {
  154. twoColumn = state.twoColumn;
  155. } else {
  156. twoColumn = this._needsCollapse(parent.clientHeight, this._entries || {});
  157. }
  158. // always update two column
  159. cls.toggle(PALETTE_TWO_COLUMN_CLS, twoColumn);
  160. parentCls.toggle(PALETTE_PREFIX + PALETTE_TWO_COLUMN_CLS, twoColumn);
  161. if ('open' in state) {
  162. cls.toggle(PALETTE_OPEN_CLS, state.open);
  163. parentCls.toggle(PALETTE_PREFIX + PALETTE_OPEN_CLS, state.open);
  164. }
  165. eventBus.fire('palette.changed', {
  166. twoColumn: twoColumn,
  167. open: this.isOpen()
  168. });
  169. };
  170. Palette.prototype._update = function() {
  171. var entriesContainer = domQuery('.djs-palette-entries', this._container),
  172. entries = this._entries = this.getEntries();
  173. domClear(entriesContainer);
  174. forEach(entries, function(entry, id) {
  175. var grouping = entry.group || 'default';
  176. var container = domQuery('[data-group=' + escapeCSS(grouping) + ']', entriesContainer);
  177. if (!container) {
  178. container = domify('<div class="group"></div>');
  179. domAttr(container, 'data-group', grouping);
  180. entriesContainer.appendChild(container);
  181. }
  182. var html = entry.html || (
  183. entry.separator ?
  184. '<hr class="separator" />' :
  185. '<div class="entry" draggable="true"></div>');
  186. var control = domify(html);
  187. container.appendChild(control);
  188. if (!entry.separator) {
  189. domAttr(control, 'data-action', id);
  190. if (entry.title) {
  191. domAttr(control, 'title', entry.title);
  192. }
  193. if (entry.className) {
  194. addClasses(control, entry.className);
  195. }
  196. if (entry.imageUrl) {
  197. var image = domify('<img>');
  198. domAttr(image, 'src', entry.imageUrl);
  199. control.appendChild(image);
  200. }
  201. }
  202. });
  203. // open after update
  204. this.open();
  205. };
  206. /**
  207. * Trigger an action available on the palette
  208. *
  209. * @param {string} action
  210. * @param {Event} event
  211. */
  212. Palette.prototype.trigger = function(action, event, autoActivate) {
  213. var entry,
  214. originalEvent,
  215. button = event.delegateTarget || event.target;
  216. if (!button) {
  217. return event.preventDefault();
  218. }
  219. entry = domAttr(button, 'data-action');
  220. originalEvent = event.originalEvent || event;
  221. return this.triggerEntry(entry, action, originalEvent, autoActivate);
  222. };
  223. Palette.prototype.triggerEntry = function(entryId, action, event, autoActivate) {
  224. var entries = this._entries,
  225. entry,
  226. handler;
  227. entry = entries[entryId];
  228. // when user clicks on the palette and not on an action
  229. if (!entry) {
  230. return;
  231. }
  232. handler = entry.action;
  233. // simple action (via callback function)
  234. if (isFunction(handler)) {
  235. if (action === 'click') {
  236. return handler(event, autoActivate);
  237. }
  238. } else {
  239. if (handler[action]) {
  240. return handler[action](event, autoActivate);
  241. }
  242. }
  243. // silence other actions
  244. event.preventDefault();
  245. };
  246. Palette.prototype._layoutChanged = function() {
  247. this._toggleState({});
  248. };
  249. /**
  250. * Do we need to collapse to two columns?
  251. *
  252. * @param {number} availableHeight
  253. * @param {Object} entries
  254. *
  255. * @return {boolean}
  256. */
  257. Palette.prototype._needsCollapse = function(availableHeight, entries) {
  258. // top margin + bottom toggle + bottom margin
  259. // implementors must override this method if they
  260. // change the palette styles
  261. var margin = 20 + 10 + 20;
  262. var entriesHeight = Object.keys(entries).length * 46;
  263. return availableHeight < entriesHeight + margin;
  264. };
  265. /**
  266. * Close the palette
  267. */
  268. Palette.prototype.close = function() {
  269. this._toggleState({
  270. open: false,
  271. twoColumn: false
  272. });
  273. };
  274. /**
  275. * Open the palette
  276. */
  277. Palette.prototype.open = function() {
  278. this._toggleState({ open: true });
  279. };
  280. Palette.prototype.toggle = function(open) {
  281. if (this.isOpen()) {
  282. this.close();
  283. } else {
  284. this.open();
  285. }
  286. };
  287. Palette.prototype.isActiveTool = function(tool) {
  288. return tool && this._activeTool === tool;
  289. };
  290. Palette.prototype.updateToolHighlight = function(name) {
  291. var entriesContainer,
  292. toolsContainer;
  293. if (!this._toolsContainer) {
  294. entriesContainer = domQuery('.djs-palette-entries', this._container);
  295. this._toolsContainer = domQuery('[data-group=tools]', entriesContainer);
  296. }
  297. toolsContainer = this._toolsContainer;
  298. forEach(toolsContainer.children, function(tool) {
  299. var actionName = tool.getAttribute('data-action');
  300. if (!actionName) {
  301. return;
  302. }
  303. var toolClasses = domClasses(tool);
  304. actionName = actionName.replace('-tool', '');
  305. if (toolClasses.contains('entry') && actionName === name) {
  306. toolClasses.add('highlighted-entry');
  307. } else {
  308. toolClasses.remove('highlighted-entry');
  309. }
  310. });
  311. };
  312. /**
  313. * Return true if the palette is opened.
  314. *
  315. * @example
  316. *
  317. * palette.open();
  318. *
  319. * if (palette.isOpen()) {
  320. * // yes, we are open
  321. * }
  322. *
  323. * @return {boolean} true if palette is opened
  324. */
  325. Palette.prototype.isOpen = function() {
  326. return domClasses(this._container).has(PALETTE_OPEN_CLS);
  327. };
  328. /**
  329. * Get container the palette lives in.
  330. *
  331. * @return {Element}
  332. */
  333. Palette.prototype._getParentContainer = function() {
  334. return this._canvas.getContainer();
  335. };
  336. /* markup definition */
  337. Palette.HTML_MARKUP =
  338. '<div class="djs-palette">' +
  339. '<div class="djs-palette-entries"></div>' +
  340. '<div class="djs-palette-toggle"></div>' +
  341. '</div>';
  342. // helpers //////////////////////
  343. function addClasses(element, classNames) {
  344. var classes = domClasses(element);
  345. var actualClassNames = isArray(classNames) ? classNames : classNames.split(/\s+/g);
  346. actualClassNames.forEach(function(cls) {
  347. classes.add(cls);
  348. });
  349. }
  350. function addPaletteEntries(entries, provider) {
  351. var entriesOrUpdater = provider.getPaletteEntries();
  352. if (isFunction(entriesOrUpdater)) {
  353. return entriesOrUpdater(entries);
  354. }
  355. forEach(entriesOrUpdater, function(entry, id) {
  356. entries[id] = entry;
  357. });
  358. return entries;
  359. }