| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- 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 || '<div class="entry" draggable="true"></div>'),
- container;
- domAttr(control, 'data-action', id);
- container = domQuery('[data-group=' + escapeCSS(grouping) + ']', html);
- if (!container) {
- container = domify('<div class="group"></div>');
- 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('<img>');
- domAttr(image, 'src', entry.imageUrl);
- image.style.width = '100%';
- image.style.height = '100%';
- control.appendChild(image);
- }
- });
- domClasses(html).add('open');
- this._current = {
- target: target,
- entries: entries,
- pad: pad
- };
- this._eventBus.fire('contextPad.open', { current: this._current });
- };
- /**
- * @param {ContextPadTarget} target
- *
- * @return {Overlay}
- */
- ContextPad.prototype.getPad = function(target) {
- if (this.isOpen()) {
- return this._current.pad;
- }
- var self = this;
- var overlays = this._overlays;
- var html = domify('<div class="djs-context-pad"></div>');
- var position = this._getPosition(target);
- var overlaysConfig = assign({
- html: html
- }, this._overlaysConfig, position);
- domDelegate.bind(html, entrySelector, 'click', function(event) {
- self.trigger('click', event);
- });
- domDelegate.bind(html, entrySelector, 'dragstart', function(event) {
- self.trigger('dragstart', event);
- });
- // stop propagation of mouse events
- domEvent.bind(html, 'mousedown', function(event) {
- event.stopPropagation();
- });
- var activeRootElement = this._canvas.getRootElement();
- this._overlayId = overlays.add(activeRootElement, 'context-pad', overlaysConfig);
- var pad = overlays.get(this._overlayId);
- this._eventBus.fire('contextPad.create', {
- target: target,
- pad: pad
- });
- return pad;
- };
- /**
- * Close the context pad
- */
- ContextPad.prototype.close = function() {
- if (!this.isOpen()) {
- return;
- }
- this._overlays.remove(this._overlayId);
- this._overlayId = null;
- this._eventBus.fire('contextPad.close', { current: this._current });
- this._current = null;
- };
- /**
- * Check if pad is open.
- *
- * If target is provided, check if it is opened
- * for the given target (single or multiple elements).
- *
- * @param {ContextPadTarget} [target]
- * @return {boolean}
- */
- ContextPad.prototype.isOpen = function(target) {
- var current = this._current;
- if (!current) {
- return false;
- }
- // basic no-args is open check
- if (!target) {
- return true;
- }
- var currentTarget = current.target;
- // strict handling of single vs. multi-selection
- if (isArray(target) !== isArray(currentTarget)) {
- return false;
- }
- if (isArray(target)) {
- return (
- target.length === currentTarget.length &&
- every(target, function(element) {
- return includes(currentTarget, element);
- })
- );
- } else {
- return currentTarget === target;
- }
- };
- /**
- * Check if pad is open and not hidden.
- *
- * @return {boolean}
- */
- ContextPad.prototype.isShown = function() {
- return this.isOpen() && this._overlays.isShown();
- };
- /**
- * Get contex pad position.
- *
- * @param {ContextPadTarget} target
- * @return {Bounds}
- */
- ContextPad.prototype._getPosition = function(target) {
- var elements = isArray(target) ? target : [ target ];
- var bBox = getBBox(elements);
- return {
- position: {
- left: bBox.x + bBox.width + CONTEXT_PAD_PADDING,
- top: bBox.y - CONTEXT_PAD_PADDING / 2
- }
- };
- };
- // helpers //////////
- function addClasses(element, classNames) {
- var classes = domClasses(element);
- classNames = isArray(classNames) ? classNames : classNames.split(/\s+/g);
- classNames.forEach(function(cls) {
- classes.add(cls);
- });
- }
- /**
- * @param {any[]} array
- * @param {any} item
- *
- * @return {boolean}
- */
- function includes(array, item) {
- return array.indexOf(item) !== -1;
- }
|