import { assign, every, forEach, isArray, isDefined, isFunction, some } from 'min-dash'; import { delegate as domDelegate, event as domEvent, attr as domAttr, query as domQuery, classes as domClasses, domify as domify } from 'min-dom'; import { getBBox } from '../../util/Elements'; import { escapeCSS } from '../../util/EscapeUtil'; var entrySelector = '.entry'; var DEFAULT_PRIORITY = 1000; var CONTEXT_PAD_PADDING = 12; /** * @typedef {djs.model.Base|djs.model.Base[]} ContextPadTarget */ /** * A context pad that displays element specific, contextual actions next * to a diagram element. * * @param {Canvas} canvas * @param {Object} config * @param {boolean|Object} [config.scale={ min: 1.0, max: 1.5 }] * @param {number} [config.scale.min] * @param {number} [config.scale.max] * @param {EventBus} eventBus * @param {Overlays} overlays */ export default function ContextPad(canvas, config, eventBus, overlays) { this._canvas = canvas; this._eventBus = eventBus; this._overlays = overlays; var scale = isDefined(config && config.scale) ? config.scale : { min: 1, max: 1.5 }; this._overlaysConfig = { scale: scale }; this._current = null; this._init(); } ContextPad.$inject = [ 'canvas', 'config.contextPad', 'eventBus', 'overlays' ]; /** * Registers events needed for interaction with other components. */ ContextPad.prototype._init = function() { var self = this; this._eventBus.on('selection.changed', function(event) { var selection = event.newSelection; var target = selection.length ? selection.length === 1 ? selection[0] : selection : null; if (target) { self.open(target, true); } else { self.close(); } }); this._eventBus.on('elements.changed', function(event) { var elements = event.elements, current = self._current; if (!current) { return; } var currentTarget = current.target; var currentChanged = some( isArray(currentTarget) ? currentTarget : [ currentTarget ], function(element) { return includes(elements, element); } ); // re-open if elements in current selection changed if (currentChanged) { self.open(currentTarget, true); } }); }; /** * Register context pad provider. * * @param {number} [priority=1000] * @param {ContextPadProvider} provider * * @example * const contextPadProvider = { * getContextPadEntries: function(element) { * return function(entries) { * return { * ...entries, * 'entry-1': { * label: 'My Entry', * action: function() { alert("I have been clicked!"); } * } * }; * } * }, * * getMultiElementContextPadEntries: function(elements) { * // ... * } * }; * * contextPad.registerProvider(800, contextPadProvider); */ ContextPad.prototype.registerProvider = function(priority, provider) { if (!provider) { provider = priority; priority = DEFAULT_PRIORITY; } this._eventBus.on('contextPad.getProviders', priority, function(event) { event.providers.push(provider); }); }; /** * Get context pad entries for given elements. * * @param {ContextPadTarget} target * * @return {ContextPadEntryDescriptor[]} list of entries */ ContextPad.prototype.getEntries = function(target) { var providers = this._getProviders(); var provideFn = isArray(target) ? 'getMultiElementContextPadEntries' : 'getContextPadEntries'; var entries = {}; // loop through all providers and their entries. // group entries by id so that overriding an entry is possible forEach(providers, function(provider) { if (!isFunction(provider[provideFn])) { return; } var entriesOrUpdater = provider[provideFn](target); if (isFunction(entriesOrUpdater)) { entries = entriesOrUpdater(entries); } else { forEach(entriesOrUpdater, function(entry, id) { entries[id] = entry; }); } }); return entries; }; /** * Trigger context pad via DOM event. * * The entry to trigger is determined by the target element. * * @param {string} action * @param {Event} event * @param {boolean} [autoActivate=false] */ ContextPad.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); }; /** * Trigger context pad entry entry. * * @param {string} entryId * @param {string} action * @param {Event} event * @param {boolean} [autoActivate=false] */ ContextPad.prototype.triggerEntry = function(entryId, action, event, autoActivate) { if (!this.isShown()) { return; } var target = this._current.target, entries = this._current.entries; var entry = entries[entryId]; if (!entry) { return; } var handler = entry.action; // simple action (via callback function) if (isFunction(handler)) { if (action === 'click') { return handler(event, target, autoActivate); } } else { if (handler[action]) { return handler[action](event, target, autoActivate); } } // silence other actions event.preventDefault(); }; /** * Open the context pad for given elements. * * @param {ContextPadTarget} target * @param {boolean} [force=false] - Force re-opening context pad. */ ContextPad.prototype.open = function(target, force) { if (!force && this.isOpen(target)) { return; } this.close(); this._updateAndOpen(target); }; ContextPad.prototype._getProviders = function() { var event = this._eventBus.createEvent({ type: 'contextPad.getProviders', providers: [] }); this._eventBus.fire(event); return event.providers; }; /** * @param {ContextPadTarget} target */ ContextPad.prototype._updateAndOpen = function(target) { var entries = this.getEntries(target), pad = this.getPad(target), html = pad.html, image; forEach(entries, function(entry, id) { var grouping = entry.group || 'default', control = domify(entry.html || '
'), container; domAttr(control, 'data-action', id); container = domQuery('[data-group=' + escapeCSS(grouping) + ']', html); if (!container) { container = domify(''); domAttr(container, 'data-group', grouping); html.appendChild(container); } container.appendChild(control); if (entry.className) { addClasses(control, entry.className); } if (entry.title) { domAttr(control, 'title', entry.title); } if (entry.imageUrl) { image = domify('