import { isArray, isFunction, forEach } from 'min-dash'; import { domify, query as domQuery, attr as domAttr, clear as domClear, classes as domClasses, matches as domMatches, delegate as domDelegate, event as domEvent } from 'min-dom'; import { escapeCSS } from '../../util/EscapeUtil'; var TOGGLE_SELECTOR = '.djs-palette-toggle', ENTRY_SELECTOR = '.entry', ELEMENT_SELECTOR = TOGGLE_SELECTOR + ', ' + ENTRY_SELECTOR; var PALETTE_PREFIX = 'djs-palette-', PALETTE_SHOWN_CLS = 'shown', PALETTE_OPEN_CLS = 'open', PALETTE_TWO_COLUMN_CLS = 'two-column'; var DEFAULT_PRIORITY = 1000; /** * A palette containing modeling elements. */ export default function Palette(eventBus, canvas) { this._eventBus = eventBus; this._canvas = canvas; var self = this; eventBus.on('tool-manager.update', function(event) { var tool = event.tool; self.updateToolHighlight(tool); }); eventBus.on('i18n.changed', function() { self._update(); }); eventBus.on('diagram.init', function() { self._diagramInitialized = true; self._rebuild(); }); } Palette.$inject = [ 'eventBus', 'canvas' ]; /** * Register a provider with the palette * * @param {number} [priority=1000] * @param {PaletteProvider} provider * * @example * const paletteProvider = { * getPaletteEntries: function() { * return function(entries) { * return { * ...entries, * 'entry-1': { * label: 'My Entry', * action: function() { alert("I have been clicked!"); } * } * }; * } * } * }; * * palette.registerProvider(800, paletteProvider); */ Palette.prototype.registerProvider = function(priority, provider) { if (!provider) { provider = priority; priority = DEFAULT_PRIORITY; } this._eventBus.on('palette.getProviders', priority, function(event) { event.providers.push(provider); }); this._rebuild(); }; /** * Returns the palette entries * * @return {Object} map of entries */ Palette.prototype.getEntries = function() { var providers = this._getProviders(); return providers.reduce(addPaletteEntries, {}); }; Palette.prototype._rebuild = function() { if (!this._diagramInitialized) { return; } var providers = this._getProviders(); if (!providers.length) { return; } if (!this._container) { this._init(); } this._update(); }; /** * Initialize */ Palette.prototype._init = function() { var self = this; var eventBus = this._eventBus; var parentContainer = this._getParentContainer(); var container = this._container = domify(Palette.HTML_MARKUP); parentContainer.appendChild(container); domClasses(parentContainer).add(PALETTE_PREFIX + PALETTE_SHOWN_CLS); domDelegate.bind(container, ELEMENT_SELECTOR, 'click', function(event) { var target = event.delegateTarget; if (domMatches(target, TOGGLE_SELECTOR)) { return self.toggle(); } self.trigger('click', event); }); // prevent drag propagation domEvent.bind(container, 'mousedown', function(event) { event.stopPropagation(); }); // prevent drag propagation domDelegate.bind(container, ENTRY_SELECTOR, 'dragstart', function(event) { self.trigger('dragstart', event); }); eventBus.on('canvas.resized', this._layoutChanged, this); eventBus.fire('palette.create', { container: container }); }; Palette.prototype._getProviders = function(id) { var event = this._eventBus.createEvent({ type: 'palette.getProviders', providers: [] }); this._eventBus.fire(event); return event.providers; }; /** * Update palette state. * * @param {Object} [state] { open, twoColumn } */ Palette.prototype._toggleState = function(state) { state = state || {}; var parent = this._getParentContainer(), container = this._container; var eventBus = this._eventBus; var twoColumn; var cls = domClasses(container), parentCls = domClasses(parent); if ('twoColumn' in state) { twoColumn = state.twoColumn; } else { twoColumn = this._needsCollapse(parent.clientHeight, this._entries || {}); } // always update two column cls.toggle(PALETTE_TWO_COLUMN_CLS, twoColumn); parentCls.toggle(PALETTE_PREFIX + PALETTE_TWO_COLUMN_CLS, twoColumn); if ('open' in state) { cls.toggle(PALETTE_OPEN_CLS, state.open); parentCls.toggle(PALETTE_PREFIX + PALETTE_OPEN_CLS, state.open); } eventBus.fire('palette.changed', { twoColumn: twoColumn, open: this.isOpen() }); }; Palette.prototype._update = function() { var entriesContainer = domQuery('.djs-palette-entries', this._container), entries = this._entries = this.getEntries(); domClear(entriesContainer); forEach(entries, function(entry, id) { var grouping = entry.group || 'default'; var container = domQuery('[data-group=' + escapeCSS(grouping) + ']', entriesContainer); if (!container) { container = domify('
'); domAttr(container, 'data-group', grouping); entriesContainer.appendChild(container); } var html = entry.html || ( entry.separator ? '
' : '
'); var control = domify(html); container.appendChild(control); if (!entry.separator) { domAttr(control, 'data-action', id); if (entry.title) { domAttr(control, 'title', entry.title); } if (entry.className) { addClasses(control, entry.className); } if (entry.imageUrl) { var image = domify(''); domAttr(image, 'src', entry.imageUrl); control.appendChild(image); } } }); // open after update this.open(); }; /** * Trigger an action available on the palette * * @param {string} action * @param {Event} event */ Palette.prototype.trigger = function(action, event, autoActivate) { var entry, originalEvent, button = event.delegateTarget || event.target; if (!button) { return event.preventDefault(); } entry = domAttr(button, 'data-action'); originalEvent = event.originalEvent || event; return this.triggerEntry(entry, action, originalEvent, autoActivate); }; Palette.prototype.triggerEntry = function(entryId, action, event, autoActivate) { var entries = this._entries, entry, handler; entry = entries[entryId]; // when user clicks on the palette and not on an action if (!entry) { return; } handler = entry.action; // simple action (via callback function) if (isFunction(handler)) { if (action === 'click') { return handler(event, autoActivate); } } else { if (handler[action]) { return handler[action](event, autoActivate); } } // silence other actions event.preventDefault(); }; Palette.prototype._layoutChanged = function() { this._toggleState({}); }; /** * Do we need to collapse to two columns? * * @param {number} availableHeight * @param {Object} entries * * @return {boolean} */ Palette.prototype._needsCollapse = function(availableHeight, entries) { // top margin + bottom toggle + bottom margin // implementors must override this method if they // change the palette styles var margin = 20 + 10 + 20; var entriesHeight = Object.keys(entries).length * 46; return availableHeight < entriesHeight + margin; }; /** * Close the palette */ Palette.prototype.close = function() { this._toggleState({ open: false, twoColumn: false }); }; /** * Open the palette */ Palette.prototype.open = function() { this._toggleState({ open: true }); }; Palette.prototype.toggle = function(open) { if (this.isOpen()) { this.close(); } else { this.open(); } }; Palette.prototype.isActiveTool = function(tool) { return tool && this._activeTool === tool; }; Palette.prototype.updateToolHighlight = function(name) { var entriesContainer, toolsContainer; if (!this._toolsContainer) { entriesContainer = domQuery('.djs-palette-entries', this._container); this._toolsContainer = domQuery('[data-group=tools]', entriesContainer); } toolsContainer = this._toolsContainer; forEach(toolsContainer.children, function(tool) { var actionName = tool.getAttribute('data-action'); if (!actionName) { return; } var toolClasses = domClasses(tool); actionName = actionName.replace('-tool', ''); if (toolClasses.contains('entry') && actionName === name) { toolClasses.add('highlighted-entry'); } else { toolClasses.remove('highlighted-entry'); } }); }; /** * Return true if the palette is opened. * * @example * * palette.open(); * * if (palette.isOpen()) { * // yes, we are open * } * * @return {boolean} true if palette is opened */ Palette.prototype.isOpen = function() { return domClasses(this._container).has(PALETTE_OPEN_CLS); }; /** * Get container the palette lives in. * * @return {Element} */ Palette.prototype._getParentContainer = function() { return this._canvas.getContainer(); }; /* markup definition */ Palette.HTML_MARKUP = '
' + '
' + '
' + '
'; // helpers ////////////////////// function addClasses(element, classNames) { var classes = domClasses(element); var actualClassNames = isArray(classNames) ? classNames : classNames.split(/\s+/g); actualClassNames.forEach(function(cls) { classes.add(cls); }); } function addPaletteEntries(entries, provider) { var entriesOrUpdater = provider.getPaletteEntries(); if (isFunction(entriesOrUpdater)) { return entriesOrUpdater(entries); } forEach(entriesOrUpdater, function(entry, id) { entries[id] = entry; }); return entries; }