| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552 |
- import {
- render,
- html
- } from '../../ui';
- import {
- domify,
- remove as domRemove,
- closest as domClosest,
- attr as domAttr
- } from 'min-dom';
- import {
- forEach,
- isFunction,
- omit,
- isDefined
- } from 'min-dash';
- import PopupMenuComponent from './PopupMenuComponent';
- var DATA_REF = 'data-id';
- var CLOSE_EVENTS = [
- 'contextPad.close',
- 'canvas.viewbox.changing',
- 'commandStack.changed'
- ];
- var DEFAULT_PRIORITY = 1000;
- /**
- * A popup menu that can be used to display a list of actions anywhere in the 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 {Canvas} canvas
- *
- * @class
- * @constructor
- */
- export default function PopupMenu(config, eventBus, canvas) {
- this._eventBus = eventBus;
- this._canvas = canvas;
- this._current = null;
- var scale = isDefined(config && config.scale) ? config.scale : {
- min: 1,
- max: 1.5
- };
- this._config = {
- scale: scale
- };
- eventBus.on('diagram.destroy', () => {
- this.close();
- });
- eventBus.on('element.changed', event => {
- const element = this.isOpen() && this._current.element;
- if (event.element === element) {
- this._render();
- }
- });
- }
- PopupMenu.$inject = [
- 'config.popupMenu',
- 'eventBus',
- 'canvas'
- ];
- PopupMenu.prototype._render = function() {
- const {
- position: _position,
- className,
- entries,
- headerEntries,
- options
- } = this._current;
- const entriesArray = Object.entries(entries).map(
- ([ key, value ]) => ({ id: key, ...value })
- );
- const headerEntriesArray = Object.entries(headerEntries).map(
- ([ key, value ]) => ({ id: key, ...value })
- );
- const position = _position && (
- (container) => this._ensureVisible(container, _position)
- );
- const scale = this._updateScale(this._current.container);
- const onClose = result => this.close(result);
- const onSelect = (event, entry, action) => this.trigger(event, entry, action);
- render(
- html`
- <${PopupMenuComponent}
- onClose=${ onClose }
- onSelect=${ onSelect }
- position=${ position }
- className=${ className }
- entries=${ entriesArray }
- headerEntries=${ headerEntriesArray }
- scale=${ scale }
- onOpened=${ this._onOpened.bind(this) }
- onClosed=${ this._onClosed.bind(this) }
- ...${{ ...options }}
- />
- `,
- this._current.container
- );
- };
- /**
- * Create entries and open popup menu at given position
- *
- * @param {Object} element
- * @param {string} id provider id
- * @param {Object} position
- *
- * @return {Object} popup menu instance
- */
- PopupMenu.prototype.open = function(element, providerId, position, options) {
- if (!element) {
- throw new Error('Element is missing');
- }
- if (!providerId) {
- throw new Error('No registered providers for: ' + providerId);
- }
- if (!position) {
- throw new Error('the position argument is missing');
- }
- if (this.isOpen()) {
- this.close();
- }
- const {
- entries,
- headerEntries
- } = this._getContext(element, providerId);
- this._current = {
- position,
- className: providerId,
- element,
- entries,
- headerEntries,
- container: this._createContainer({ provider: providerId }),
- options
- };
- this._emit('open');
- this._bindAutoClose();
- this._render();
- };
- PopupMenu.prototype._getContext = function(element, provider) {
- const providers = this._getProviders(provider);
- if (!providers || !providers.length) {
- throw new Error('No registered providers for: ' + provider);
- }
- const entries = this._getEntries(element, providers);
- const headerEntries = this._getHeaderEntries(element, providers);
- return {
- entries,
- headerEntries,
- empty: !(
- Object.keys(entries).length ||
- Object.keys(headerEntries).length
- )
- };
- };
- PopupMenu.prototype.close = function() {
- if (!this.isOpen()) {
- return;
- }
- this._emit('close');
- this.reset();
- this._current = null;
- };
- PopupMenu.prototype.reset = function() {
- const container = this._current.container;
- render(null, container);
- domRemove(container);
- };
- PopupMenu.prototype._emit = function(event, payload) {
- this._eventBus.fire(`popupMenu.${ event }`, payload);
- };
- PopupMenu.prototype._onOpened = function() {
- this._emit('opened');
- };
- PopupMenu.prototype._onClosed = function() {
- this._emit('closed');
- };
- PopupMenu.prototype._createContainer = function(config) {
- var canvas = this._canvas,
- parent = canvas.getContainer();
- const container = domify(`<div class="djs-popup-parent djs-scrollable" data-popup=${config.provider}></div>`);
- parent.appendChild(container);
- return container;
- };
- /**
- * Set up listener to close popup automatically on certain events.
- */
- PopupMenu.prototype._bindAutoClose = function() {
- this._eventBus.once(CLOSE_EVENTS, this.close, this);
- };
- /**
- * Remove the auto-closing listener.
- */
- PopupMenu.prototype._unbindAutoClose = function() {
- this._eventBus.off(CLOSE_EVENTS, this.close, this);
- };
- /**
- * Updates popup style.transform with respect to the config and zoom level.
- *
- * @param {Object} container
- */
- PopupMenu.prototype._updateScale = function(container) {
- var zoom = this._canvas.zoom();
- var scaleConfig = this._config.scale,
- minScale,
- maxScale,
- scale = zoom;
- if (scaleConfig !== true) {
- if (scaleConfig === false) {
- minScale = 1;
- maxScale = 1;
- } else {
- minScale = scaleConfig.min;
- maxScale = scaleConfig.max;
- }
- if (isDefined(minScale) && zoom < minScale) {
- scale = minScale;
- }
- if (isDefined(maxScale) && zoom > maxScale) {
- scale = maxScale;
- }
- }
- return scale;
- };
- PopupMenu.prototype._ensureVisible = function(container, position) {
- var documentBounds = document.documentElement.getBoundingClientRect();
- var containerBounds = container.getBoundingClientRect();
- var overAxis = {},
- left = position.x,
- top = position.y;
- if (position.x + containerBounds.width > documentBounds.width) {
- overAxis.x = true;
- }
- if (position.y + containerBounds.height > documentBounds.height) {
- overAxis.y = true;
- }
- if (overAxis.x && overAxis.y) {
- left = position.x - containerBounds.width;
- top = position.y - containerBounds.height;
- } else if (overAxis.x) {
- left = position.x - containerBounds.width;
- top = position.y;
- } else if (overAxis.y && position.y < containerBounds.height) {
- left = position.x;
- top = 10;
- } else if (overAxis.y) {
- left = position.x;
- top = position.y - containerBounds.height;
- }
- return {
- x: left,
- y: top
- };
- };
- PopupMenu.prototype.isEmpty = function(element, providerId) {
- if (!element) {
- throw new Error('element parameter is missing');
- }
- if (!providerId) {
- throw new Error('providerId parameter is missing');
- }
- const providers = this._getProviders(providerId);
- if (!providers || !providers.length) {
- return true;
- }
- return this._getContext(element, providerId).empty;
- };
- /**
- * Registers a popup menu provider
- *
- * @param {string} id
- * @param {number} [priority=1000]
- * @param {Object} provider
- *
- * @example
- * const popupMenuProvider = {
- * getPopupMenuEntries(element) {
- * return {
- * 'entry-1': {
- * label: 'My Entry',
- * action: function() { alert("I have been clicked!"); }
- * }
- * }
- * }
- * };
- *
- * popupMenu.registerProvider('myMenuID', popupMenuProvider);
- *
- * @example
- * const replacingPopupMenuProvider = {
- * getPopupMenuEntries(element) {
- * return (entries) => {
- * const {
- * someEntry,
- * ...remainingEntries
- * } = entries;
- *
- * return remainingEntries;
- * };
- * }
- * };
- *
- * popupMenu.registerProvider('myMenuID', replacingPopupMenuProvider);
- */
- PopupMenu.prototype.registerProvider = function(id, priority, provider) {
- if (!provider) {
- provider = priority;
- priority = DEFAULT_PRIORITY;
- }
- this._eventBus.on('popupMenu.getProviders.' + id, priority, function(event) {
- event.providers.push(provider);
- });
- };
- PopupMenu.prototype._getProviders = function(id) {
- var event = this._eventBus.createEvent({
- type: 'popupMenu.getProviders.' + id,
- providers: []
- });
- this._eventBus.fire(event);
- return event.providers;
- };
- PopupMenu.prototype._getEntries = function(element, providers) {
- var entries = {};
- forEach(providers, function(provider) {
- // handle legacy method
- if (!provider.getPopupMenuEntries) {
- forEach(provider.getEntries(element), function(entry) {
- var id = entry.id;
- if (!id) {
- throw new Error('every entry must have the id property set');
- }
- entries[id] = omit(entry, [ 'id' ]);
- });
- return;
- }
- var entriesOrUpdater = provider.getPopupMenuEntries(element);
- if (isFunction(entriesOrUpdater)) {
- entries = entriesOrUpdater(entries);
- } else {
- forEach(entriesOrUpdater, function(entry, id) {
- entries[id] = entry;
- });
- }
- });
- return entries;
- };
- PopupMenu.prototype._getHeaderEntries = function(element, providers) {
- var entries = {};
- forEach(providers, function(provider) {
- // handle legacy method
- if (!provider.getPopupMenuHeaderEntries) {
- if (!provider.getHeaderEntries) {
- return;
- }
- forEach(provider.getHeaderEntries(element), function(entry) {
- var id = entry.id;
- if (!id) {
- throw new Error('every entry must have the id property set');
- }
- entries[id] = omit(entry, [ 'id' ]);
- });
- return;
- }
- var entriesOrUpdater = provider.getPopupMenuHeaderEntries(element);
- if (isFunction(entriesOrUpdater)) {
- entries = entriesOrUpdater(entries);
- } else {
- forEach(entriesOrUpdater, function(entry, id) {
- entries[id] = entry;
- });
- }
- });
- return entries;
- };
- /**
- * Determine if an open popup menu exist.
- *
- * @return {boolean} true if open
- */
- PopupMenu.prototype.isOpen = function() {
- return !!this._current;
- };
- /**
- * Trigger an action associated with an entry.
- *
- * @param {Object} event
- * @param {Object} entry
- * @param {string} [action='click'] the action to trigger
- *
- * @return the result of the action callback, if any
- */
- PopupMenu.prototype.trigger = function(event, entry, action = 'click') {
- // silence other actions
- event.preventDefault();
- if (!entry) {
- let element = domClosest(event.delegateTarget || event.target, '.entry', true);
- let entryId = domAttr(element, DATA_REF);
- entry = this._getEntry(entryId);
- }
- const handler = entry.action;
- if (isFunction(handler)) {
- if (action === 'click') {
- return handler(event, entry);
- }
- } else {
- if (handler[action]) {
- return handler[action](event, entry);
- }
- }
- };
- /**
- * Gets an entry instance (either entry or headerEntry) by id.
- *
- * @param {string} entryId
- *
- * @return {Object} entry instance
- */
- PopupMenu.prototype._getEntry = function(entryId) {
- var entry = this._current.entries[entryId] || this._current.headerEntries[entryId];
- if (!entry) {
- throw new Error('entry not found');
- }
- return entry;
- };
|