diagram-js
Advanced tools
@@ -74,3 +74,3 @@ import { | ||
const parent = document.createElement('div'); | ||
parent.setAttribute('class', 'djs-container'); | ||
parent.setAttribute('class', 'djs-container djs-parent'); | ||
@@ -77,0 +77,0 @@ assignStyle(parent, { |
import { | ||
assign, | ||
render, | ||
html | ||
} from '../../ui'; | ||
import { | ||
domify, | ||
remove as domRemove, | ||
attr as domAttr | ||
} from 'min-dom'; | ||
import { | ||
forEach, | ||
isFunction, | ||
isDefined, | ||
omit, | ||
size | ||
isDefined | ||
} from 'min-dash'; | ||
import { | ||
assignStyle, | ||
delegate as domDelegate, | ||
domify as domify, | ||
classes as domClasses, | ||
attr as domAttr, | ||
query as domQuery, | ||
remove as domRemove | ||
} from 'min-dom'; | ||
import PopupMenuComponent from './PopupMenuComponent'; | ||
import { | ||
escapeCSS | ||
} from '../../util/EscapeUtil'; | ||
@@ -49,2 +47,5 @@ var DATA_REF = 'data-id'; | ||
export default function PopupMenu(config, eventBus, canvas) { | ||
this._eventBus = eventBus; | ||
this._canvas = canvas; | ||
this._current = {}; | ||
@@ -60,6 +61,16 @@ var scale = isDefined(config && config.scale) ? config.scale : { | ||
this._eventBus = eventBus; | ||
this._canvas = canvas; | ||
this._providers = {}; | ||
this._current = {}; | ||
eventBus.on('diagram.destroy', () => { | ||
this._destroy(); | ||
}); | ||
eventBus.on('element.changed', event => { | ||
const element = this.isOpen() && this._current.element; | ||
if (event.element === element) { | ||
this._render(); | ||
} | ||
}); | ||
} | ||
@@ -73,61 +84,39 @@ | ||
/** | ||
* Registers a popup menu provider | ||
* | ||
* @param {string} id | ||
* @param {number} [priority=1000] | ||
* @param {Object} provider | ||
* | ||
* @example | ||
* const popupMenuProvider = { | ||
* getPopupMenuEntries: function(element) { | ||
* return { | ||
* 'entry-1': { | ||
* label: 'My Entry', | ||
* action: function() { alert("I have been clicked!"); } | ||
* } | ||
* } | ||
* } | ||
* }; | ||
* | ||
* popupMenu.registerProvider('myMenuID', popupMenuProvider); | ||
*/ | ||
PopupMenu.prototype.registerProvider = function(id, priority, provider) { | ||
if (!provider) { | ||
provider = priority; | ||
priority = DEFAULT_PRIORITY; | ||
} | ||
PopupMenu.prototype._render = function() { | ||
this._eventBus.on('popupMenu.getProviders.' + id, priority, function(event) { | ||
event.providers.push(provider); | ||
}); | ||
}; | ||
const { | ||
position: _position, | ||
className, | ||
entries, | ||
headerEntries, | ||
options | ||
} = this._current; | ||
/** | ||
* 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'); | ||
} | ||
const entriesArray = [ | ||
...Object.entries(entries).map( | ||
([ key, value ]) => ({ id: key, ...value }) | ||
) | ||
]; | ||
if (!providerId) { | ||
throw new Error('providerId parameter is missing'); | ||
} | ||
const position = _position && ( | ||
(container) => this._ensureVisible(container, _position) | ||
); | ||
var providers = this._getProviders(providerId); | ||
const scale = this._updateScale(this._current.container); | ||
if (!providers) { | ||
return true; | ||
} | ||
const onClose = result => this.close(result); | ||
var entries = this._getEntries(element, providers), | ||
headerEntries = this._getHeaderEntries(element, providers); | ||
var hasEntries = size(entries) > 0, | ||
hasHeaderEntries = headerEntries && size(headerEntries) > 0; | ||
return !hasEntries && !hasHeaderEntries; | ||
render(html` | ||
<${PopupMenuComponent} | ||
onClose=${ onClose } | ||
position=${ position } | ||
className=${ className } | ||
entries=${ entriesArray } | ||
headerEntries=${ headerEntries } | ||
scale=${ scale } | ||
...${{ ...options }} | ||
/> | ||
`, | ||
this._current.container | ||
); | ||
}; | ||
@@ -145,6 +134,3 @@ | ||
*/ | ||
PopupMenu.prototype.open = function(element, id, position) { | ||
var providers = this._getProviders(id); | ||
PopupMenu.prototype.open = function(element, providerId, position, options) { | ||
if (!element) { | ||
@@ -154,4 +140,4 @@ throw new Error('Element is missing'); | ||
if (!providers || !providers.length) { | ||
throw new Error('No registered providers for: ' + id); | ||
if (!providerId) { | ||
throw new Error('No registered providers for: ' + providerId); | ||
} | ||
@@ -166,41 +152,48 @@ | ||
} | ||
const { | ||
entries, | ||
headerEntries | ||
} = this._getContext(element, providerId); | ||
this._current = { | ||
position, | ||
className: providerId, | ||
element, | ||
entries, | ||
headerEntries, | ||
container: this._createContainer({ provider: providerId }), | ||
options | ||
}; | ||
this._emit('open'); | ||
var current = this._current = { | ||
className: id, | ||
element: element, | ||
position: position | ||
}; | ||
this._bindAutoClose(); | ||
var entries = this._getEntries(element, providers), | ||
headerEntries = this._getHeaderEntries(element, providers); | ||
this._render(); | ||
current.entries = assign({}, entries, headerEntries); | ||
current.container = this._createContainer(id); | ||
}; | ||
if (size(headerEntries)) { | ||
current.container.appendChild( | ||
this._createEntries(headerEntries, 'djs-popup-header') | ||
); | ||
} | ||
if (size(entries)) { | ||
current.container.appendChild( | ||
this._createEntries(entries, 'djs-popup-body') | ||
); | ||
PopupMenu.prototype._getContext = function(element, provider) { | ||
const providers = this._getProviders(provider); | ||
if (!providers || !providers.length) { | ||
throw new Error('No registered providers for: ' + provider); | ||
} | ||
var canvas = this._canvas, | ||
parent = canvas.getContainer(); | ||
const entries = this._getEntries(element, providers); | ||
const headerEntries = this._getHeaderEntries(element, providers); | ||
this._attachContainer(current.container, parent, position.cursor); | ||
this._bindAutoClose(); | ||
return { | ||
entries, | ||
headerEntries, | ||
empty: !( | ||
Object.keys(entries).length || | ||
Object.keys(headerEntries).length | ||
) | ||
}; | ||
}; | ||
/** | ||
* Removes the popup menu and unbinds the event handlers. | ||
*/ | ||
PopupMenu.prototype.close = function() { | ||
@@ -214,15 +207,35 @@ | ||
this._unbindAutoClose(); | ||
domRemove(this._current.container); | ||
this.reset(); | ||
this._current.container = null; | ||
}; | ||
PopupMenu.prototype.reset = function() { | ||
render(null, this._current.container); | ||
}; | ||
PopupMenu.prototype._emit = function(event, payload) { | ||
this._eventBus.fire(`popupMenu.${ event }`, payload); | ||
}; | ||
PopupMenu.prototype._createContainer = function(config) { | ||
let parent = config && config.parent || 'body'; | ||
if (typeof parent === 'string') { | ||
parent = document.querySelector(parent); | ||
} | ||
const container = domify(`<div class="djs-popup-parent djs-parent" data-popup=${config.provider}></div>`); | ||
parent.appendChild(container); | ||
return container; | ||
}; | ||
/** | ||
* Determine if an open popup menu exist. | ||
* | ||
* @return {boolean} true if open | ||
* Set up listener to close popup automatically on certain events. | ||
*/ | ||
PopupMenu.prototype.isOpen = function() { | ||
return !!this._current.container; | ||
PopupMenu.prototype._bindAutoClose = function() { | ||
this._eventBus.once(CLOSE_EVENTS, this.close, this); | ||
}; | ||
@@ -232,23 +245,138 @@ | ||
/** | ||
* Trigger an action associated with an entry. | ||
* Remove the auto-closing listener. | ||
*/ | ||
PopupMenu.prototype._unbindAutoClose = function() { | ||
this._eventBus.off(CLOSE_EVENTS, this.close, this); | ||
}; | ||
PopupMenu.prototype._destroy = function() { | ||
this._current.container && domRemove(this._current.container); | ||
}; | ||
/** | ||
* Updates popup style.transform with respect to the config and zoom level. | ||
* | ||
* @param {Object} event | ||
* | ||
* @return the result of the action callback, if any | ||
* @param {Object} container | ||
*/ | ||
PopupMenu.prototype.trigger = function(event) { | ||
PopupMenu.prototype._updateScale = function(container) { | ||
var zoom = this._canvas.zoom(); | ||
// silence other actions | ||
event.preventDefault(); | ||
var scaleConfig = this._config.scale, | ||
minScale, | ||
maxScale, | ||
scale = zoom; | ||
var element = event.delegateTarget || event.target, | ||
entryId = domAttr(element, DATA_REF); | ||
if (scaleConfig !== true) { | ||
var entry = this._getEntry(entryId); | ||
if (scaleConfig === false) { | ||
minScale = 1; | ||
maxScale = 1; | ||
} else { | ||
minScale = scaleConfig.min; | ||
maxScale = scaleConfig.max; | ||
} | ||
if (entry.action) { | ||
return entry.action.call(null, event, entry); | ||
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: function(element) { | ||
* return { | ||
* 'entry-1': { | ||
* label: 'My Entry', | ||
* action: function() { alert("I have been clicked!"); } | ||
* } | ||
* } | ||
* } | ||
* }; | ||
* | ||
* popupMenu.registerProvider('myMenuID', popupMenuProvider); | ||
*/ | ||
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) { | ||
@@ -342,46 +470,10 @@ | ||
/** | ||
* 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]; | ||
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. | ||
* Determine if an open popup menu exist. | ||
* | ||
* @return {Object} a DOM container | ||
* @return {boolean} true if open | ||
*/ | ||
PopupMenu.prototype._createContainer = function(id) { | ||
var container = domify('<div class="djs-popup">'), | ||
position = this._current.position, | ||
className = this._current.className; | ||
assignStyle(container, { | ||
position: 'absolute', | ||
left: position.x + 'px', | ||
top: position.y + 'px', | ||
visibility: 'hidden' | ||
}); | ||
domClasses(container).add(className); | ||
domAttr(container, 'data-popup', id); | ||
return container; | ||
PopupMenu.prototype.isOpen = function() { | ||
return !!this._current.container; | ||
}; | ||
@@ -391,228 +483,40 @@ | ||
/** | ||
* Attaches the container to the DOM. | ||
* Trigger an action associated with an entry. | ||
* | ||
* @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); | ||
} | ||
// display after position adjustment to avoid flickering | ||
assignStyle(container, { visibility: 'visible' }); | ||
}; | ||
/** | ||
* Updates popup style.transform with respect to the config and zoom level. | ||
* @param {Object} event | ||
* | ||
* @method _updateScale | ||
* | ||
* @param {Object} container | ||
* @return the result of the action callback, if any | ||
*/ | ||
PopupMenu.prototype._updateScale = function(container) { | ||
var zoom = this._canvas.zoom(); | ||
PopupMenu.prototype.trigger = function(event) { | ||
var scaleConfig = this._config.scale, | ||
minScale, | ||
maxScale, | ||
scale = zoom; | ||
// silence other actions | ||
event.preventDefault(); | ||
if (scaleConfig !== true) { | ||
var element = event.delegateTarget || event.target, | ||
entryId = domAttr(element, DATA_REF); | ||
if (scaleConfig === false) { | ||
minScale = 1; | ||
maxScale = 1; | ||
} else { | ||
minScale = scaleConfig.min; | ||
maxScale = scaleConfig.max; | ||
} | ||
var entry = this._getEntry(entryId); | ||
if (isDefined(minScale) && zoom < minScale) { | ||
scale = minScale; | ||
} | ||
if (isDefined(maxScale) && zoom > maxScale) { | ||
scale = maxScale; | ||
} | ||
if (entry.action) { | ||
return entry.action.call(null, event, entry); | ||
} | ||
setTransform(container, 'scale(' + scale + ')'); | ||
}; | ||
/** | ||
* Make sure that the menu is always fully shown | ||
* Gets an entry instance (either entry or headerEntry) by id. | ||
* | ||
* @method function | ||
* @param {string} entryId | ||
* | ||
* @param {Object} container | ||
* @param {Position} cursor {x, y} | ||
* @return {Object} entry instance | ||
*/ | ||
PopupMenu.prototype._assureIsInbounds = function(container, cursor) { | ||
var canvas = this._canvas, | ||
clientRect = canvas._container.getBoundingClientRect(); | ||
PopupMenu.prototype._getEntry = function(entryId) { | ||
var containerX = container.offsetLeft, | ||
containerY = container.offsetTop, | ||
containerWidth = container.scrollWidth, | ||
containerHeight = container.scrollHeight, | ||
overAxis = {}, | ||
left, top; | ||
var entry = this._current.entries[entryId]; | ||
var cursorPosition = { | ||
x: cursor.x - clientRect.left, | ||
y: cursor.y - clientRect.top | ||
}; | ||
if (containerX + containerWidth > clientRect.width) { | ||
overAxis.x = true; | ||
if (!entry) { | ||
throw new Error('entry not found'); | ||
} | ||
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'; | ||
} | ||
assignStyle(container, { left: left, top: top }, { 'zIndex': 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, id) { | ||
var entryContainer = self._createEntry(entry, id), | ||
grouping = entry.group || 'default', | ||
groupContainer = domQuery('[data-group=' + escapeCSS(grouping) + ']', entriesContainer); | ||
if (!groupContainer) { | ||
groupContainer = domify('<div class="group"></div>'); | ||
domAttr(groupContainer, 'data-group', grouping); | ||
entriesContainer.appendChild(groupContainer); | ||
} | ||
groupContainer.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, id) { | ||
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, id); | ||
if (entry.label) { | ||
var label = domify('<span>'); | ||
label.textContent = entry.label; | ||
entryContainer.appendChild(label); | ||
} | ||
if (entry.imageUrl) { | ||
var image = domify('<img>'); | ||
domAttr(image, 'src', entry.imageUrl); | ||
entryContainer.appendChild(image); | ||
} | ||
if (entry.active === true) { | ||
entryClasses.add('active'); | ||
} | ||
if (entry.disabled === true) { | ||
entryClasses.add('disabled'); | ||
} | ||
if (entry.title) { | ||
entryContainer.title = entry.title; | ||
} | ||
return entryContainer; | ||
}; | ||
/** | ||
* 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); | ||
}; | ||
// helpers ///////////////////////////// | ||
function setTransform(element, transform) { | ||
element.style['transform-origin'] = 'top left'; | ||
[ '', '-ms-', '-webkit-' ].forEach(function(prefix) { | ||
element.style[prefix + 'transform'] = transform; | ||
}); | ||
} | ||
return entry; | ||
}; |
@@ -17,2 +17,29 @@ /** | ||
/** | ||
* This method should implement the creation or update of a map of entry objects. | ||
* | ||
* @param {djs.model.Base} element | ||
* | ||
* The following example shows how to replace any entries returned | ||
* by previous providers with one entry which alerts the id of the current selected | ||
* element, when clicking on the entry. | ||
* | ||
* @example | ||
* PopupMenuProvider.getPopupMenuEntries = function(element) { | ||
* return function(entries) { | ||
* return { | ||
* alert: { | ||
* label: 'Alert element ID', | ||
* className: 'alert', | ||
* action: function () { | ||
* alert(element.id); | ||
* } | ||
* } | ||
* }; | ||
* }; | ||
*/ | ||
PopupMenuProvider.getPopupMenuEntries = function(element) {}; | ||
/** | ||
* @deprecated | ||
* This method should implement the creation of a list of entry objects. | ||
@@ -37,3 +64,3 @@ * | ||
* return entries; | ||
*} | ||
* }; | ||
*/ | ||
@@ -76,2 +103,2 @@ PopupMenuProvider.getEntries = function(element) {}; | ||
*/ | ||
PopupMenuProvider.register = function() {}; | ||
PopupMenuProvider.register = function() {}; |
{ | ||
"name": "diagram-js", | ||
"version": "10.0.0", | ||
"version": "11.0.0-aplha.0", | ||
"description": "A toolbox for displaying and modifying diagrams on the web", | ||
@@ -45,6 +45,6 @@ "main": "index.js", | ||
"devDependencies": { | ||
"@babel/core": "^7.18.10", | ||
"babel-loader": "^8.2.5", | ||
"@babel/core": "^7.20.2", | ||
"babel-loader": "^9.1.0", | ||
"babel-plugin-istanbul": "^6.1.1", | ||
"chai": "^4.2.0", | ||
"chai": "^4.3.6", | ||
"eslint": "^8.24.0", | ||
@@ -62,7 +62,7 @@ "eslint-plugin-bpmn-io": "^0.16.0", | ||
"karma-webpack": "^5.0.0", | ||
"mocha": "^9.2.1", | ||
"mocha": "^10.1.0", | ||
"mocha-test-container-support": "^0.2.0", | ||
"npm-run-all": "^4.1.2", | ||
"puppeteer": "^16.1.1", | ||
"sinon": "^7.5.0", | ||
"puppeteer": "^19.0.0", | ||
"sinon": "^9.2.4", | ||
"sinon-chai": "^3.7.0", | ||
@@ -72,2 +72,4 @@ "webpack": "^5.74.0" | ||
"dependencies": { | ||
"@bpmn-io/diagram-js-ui": "^0.2.0", | ||
"clsx": "^1.2.1", | ||
"css.escape": "^1.5.1", | ||
@@ -74,0 +76,0 @@ "didi": "^9.0.0", |
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
671169
2.05%218
2.35%22916
2.2%11
22.22%2
100%1
Infinity%