123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509 |
- import {
- forEach,
- assign,
- find,
- matchPattern,
- isDefined
- } from 'min-dash';
- import {
- delegate as domDelegate,
- domify as domify,
- classes as domClasses,
- attr as domAttr,
- remove as domRemove
- } from 'min-dom';
- var DATA_REF = 'data-id';
- /**
- * 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) {
- var scale = isDefined(config && config.scale) ? config.scale : {
- min: 1,
- max: 1.5
- };
- this._config = {
- scale: scale
- };
- this._eventBus = eventBus;
- this._canvas = canvas;
- this._providers = {};
- this._current = {};
- }
- PopupMenu.$inject = [
- 'config.popupMenu',
- 'eventBus',
- 'canvas'
- ];
- /**
- * Registers a popup menu provider
- *
- * @param {String} id
- * @param {Object} provider
- *
- * @example
- *
- * popupMenu.registerProvider('myMenuID', {
- * getEntries: function(element) {
- * return [
- * {
- * id: 'entry-1',
- * label: 'My Entry',
- * action: 'alert("I have been clicked!")'
- * }
- * ];
- * }
- * });
- */
- PopupMenu.prototype.registerProvider = function(id, provider) {
- this._providers[id] = provider;
- };
- /**
- * Determine if the popup menu has entries.
- *
- * @return {Boolean} true if empty
- */
- PopupMenu.prototype.isEmpty = function(element, providerId) {
- if (!element) {
- throw new Error('element parameter is missing');
- }
- if (!providerId) {
- throw new Error('providerId parameter is missing');
- }
- var provider = this._providers[providerId];
- var entries = provider.getEntries(element),
- headerEntries = provider.getHeaderEntries && provider.getHeaderEntries(element);
- var hasEntries = entries.length > 0,
- hasHeaderEntries = headerEntries && headerEntries.length > 0;
- return !hasEntries && !hasHeaderEntries;
- };
- /**
- * 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, id, position) {
- var provider = this._providers[id];
- if (!element) {
- throw new Error('Element is missing');
- }
- if (!provider) {
- throw new Error('Provider is not registered: ' + id);
- }
- if (!position) {
- throw new Error('the position argument is missing');
- }
- if (this.isOpen()) {
- this.close();
- }
- this._emit('open');
- var current = this._current = {
- provider: provider,
- className: id,
- element: element,
- position: position
- };
- if (provider.getHeaderEntries) {
- current.headerEntries = provider.getHeaderEntries(element);
- }
- current.entries = provider.getEntries(element);
- current.container = this._createContainer();
- var headerEntries = current.headerEntries || [],
- entries = current.entries || [];
- if (headerEntries.length) {
- current.container.appendChild(
- this._createEntries(current.headerEntries, 'djs-popup-header')
- );
- }
- if (entries.length) {
- current.container.appendChild(
- this._createEntries(current.entries, 'djs-popup-body')
- );
- }
- var canvas = this._canvas,
- parent = canvas.getContainer();
- this._attachContainer(current.container, parent, position.cursor);
- };
- /**
- * Removes the popup menu and unbinds the event handlers.
- */
- PopupMenu.prototype.close = function() {
- if (!this.isOpen()) {
- return;
- }
- this._emit('close');
- this._unbindHandlers();
- domRemove(this._current.container);
- this._current.container = null;
- };
- /**
- * Determine if an open popup menu exist.
- *
- * @return {Boolean} true if open
- */
- PopupMenu.prototype.isOpen = function() {
- return !!this._current.container;
- };
- /**
- * Trigger an action associated with an entry.
- *
- * @param {Object} event
- *
- * @return the result of the action callback, if any
- */
- PopupMenu.prototype.trigger = function(event) {
- // silence other actions
- event.preventDefault();
- var element = event.delegateTarget || event.target,
- entryId = domAttr(element, DATA_REF);
- var entry = this._getEntry(entryId);
- if (entry.action) {
- return entry.action.call(null, 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 search = matchPattern({ id: entryId });
- var entry = find(this._current.entries, search) || find(this._current.headerEntries, search);
- if (!entry) {
- throw new Error('entry not found');
- }
- return entry;
- };
- PopupMenu.prototype._emit = function(eventName) {
- this._eventBus.fire('popupMenu.' + eventName);
- };
- /**
- * Creates the popup menu container.
- *
- * @return {Object} a DOM container
- */
- PopupMenu.prototype._createContainer = function() {
- var container = domify('<div class="djs-popup">'),
- position = this._current.position,
- className = this._current.className;
- assign(container.style, {
- position: 'absolute',
- left: position.x + 'px',
- top: position.y + 'px',
- visibility: 'hidden'
- });
- domClasses(container).add(className);
- return container;
- };
- /**
- * Attaches the container to the DOM and binds the event handlers.
- *
- * @param {Object} container
- * @param {Object} parent
- */
- PopupMenu.prototype._attachContainer = function(container, parent, cursor) {
- var self = this;
- // Event handler
- domDelegate.bind(container, '.entry' ,'click', function(event) {
- self.trigger(event);
- });
- this._updateScale(container);
- // Attach to DOM
- parent.appendChild(container);
- if (cursor) {
- this._assureIsInbounds(container, cursor);
- }
- // Add Handler
- this._bindHandlers();
- };
- /**
- * Updates popup style.transform with respect to the config and zoom level.
- *
- * @method _updateScale
- *
- * @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;
- }
- }
- setTransform(container, 'scale(' + scale + ')');
- };
- /**
- * Make sure that the menu is always fully shown
- *
- * @method function
- *
- * @param {Object} container
- * @param {Position} cursor {x, y}
- */
- PopupMenu.prototype._assureIsInbounds = function(container, cursor) {
- var canvas = this._canvas,
- clientRect = canvas._container.getBoundingClientRect();
- var containerX = container.offsetLeft,
- containerY = container.offsetTop,
- containerWidth = container.scrollWidth,
- containerHeight = container.scrollHeight,
- overAxis = {},
- left, top;
- var cursorPosition = {
- x: cursor.x - clientRect.left,
- y: cursor.y - clientRect.top
- };
- if (containerX + containerWidth > clientRect.width) {
- overAxis.x = true;
- }
- if (containerY + containerHeight > clientRect.height) {
- overAxis.y = true;
- }
- if (overAxis.x && overAxis.y) {
- left = cursorPosition.x - containerWidth + 'px';
- top = cursorPosition.y - containerHeight + 'px';
- } else if (overAxis.x) {
- left = cursorPosition.x - containerWidth + 'px';
- top = cursorPosition.y + 'px';
- } else if (overAxis.y && cursorPosition.y < containerHeight) {
- left = cursorPosition.x + 'px';
- top = 10 + 'px';
- } else if (overAxis.y) {
- left = cursorPosition.x + 'px';
- top = cursorPosition.y - containerHeight + 'px';
- }
- assign(container.style, { left: left, top: top }, { visibility: 'visible', 'z-index': 1000 });
- };
- /**
- * Creates a list of entries and returns them as a DOM container.
- *
- * @param {Array<Object>} entries an array of entry objects
- * @param {String} className the class name of the entry container
- *
- * @return {Object} a DOM container
- */
- PopupMenu.prototype._createEntries = function(entries, className) {
- var entriesContainer = domify('<div>'),
- self = this;
- domClasses(entriesContainer).add(className);
- forEach(entries, function(entry) {
- var entryContainer = self._createEntry(entry, entriesContainer);
- entriesContainer.appendChild(entryContainer);
- });
- return entriesContainer;
- };
- /**
- * Creates a single entry and returns it as a DOM container.
- *
- * @param {Object} entry
- *
- * @return {Object} a DOM container
- */
- PopupMenu.prototype._createEntry = function(entry) {
- if (!entry.id) {
- throw new Error ('every entry must have the id property set');
- }
- var entryContainer = domify('<div>'),
- entryClasses = domClasses(entryContainer);
- entryClasses.add('entry');
- if (entry.className) {
- entry.className.split(' ').forEach(function(className) {
- entryClasses.add(className);
- });
- }
- domAttr(entryContainer, DATA_REF, entry.id);
- if (entry.label) {
- var label = domify('<span>');
- label.textContent = entry.label;
- entryContainer.appendChild(label);
- }
- if (entry.imageUrl) {
- entryContainer.appendChild(domify('<img src="' + entry.imageUrl + '" />'));
- }
- if (entry.active === true) {
- entryClasses.add('active');
- }
- if (entry.disabled === true) {
- entryClasses.add('disabled');
- }
- if (entry.title) {
- entryContainer.title = entry.title;
- }
- return entryContainer;
- };
- /**
- * Binds the `close` method to 'contextPad.close' & 'canvas.viewbox.changed'.
- */
- PopupMenu.prototype._bindHandlers = function() {
- var eventBus = this._eventBus,
- self = this;
- function close() {
- self.close();
- }
- eventBus.once('contextPad.close', close);
- eventBus.once('canvas.viewbox.changing', close);
- eventBus.once('commandStack.changed', close);
- };
- /**
- * Unbinds the `close` method to 'contextPad.close' & 'canvas.viewbox.changing'.
- */
- PopupMenu.prototype._unbindHandlers = function() {
- var eventBus = this._eventBus,
- self = this;
- function close() {
- self.close();
- }
- eventBus.off('contextPad.close', close);
- eventBus.off('canvas.viewbox.changed', close);
- eventBus.off('commandStack.changed', close);
- };
- // helpers /////////////////////////////
- function setTransform(element, transform) {
- element.style['transform-origin'] = 'top left';
- [ '', '-ms-', '-webkit-' ].forEach(function(prefix) {
- element.style[prefix + 'transform'] = transform;
- });
- }
|