UNPKG

diagram-js

Version:

A toolbox for displaying and modifying diagrams on the web

556 lines (434 loc) 12.7 kB
import { uniqueBy, isArray } from 'min-dash'; /** * @typedef {import('didi').Injector} Injector * * @typedef {import('../core/Types').ElementLike} ElementLike * * @typedef {import('../core/EventBus').default} EventBus * @typedef {import('./CommandHandler').default} CommandHandler * * @typedef { any } CommandContext * @typedef { { * new (...args: any[]) : CommandHandler * } } CommandHandlerConstructor * @typedef { { * [key: string]: CommandHandler; * } } CommandHandlerMap * @typedef { { * command: string; * context: any; * id?: any; * } } CommandStackAction * @typedef { { * actions: CommandStackAction[]; * dirty: ElementLike[]; * trigger: 'execute' | 'undo' | 'redo' | 'clear' | null; * atomic?: boolean; * } } CurrentExecution */ /** * A service that offers un- and redoable execution of commands. * * The command stack is responsible for executing modeling actions * in a un- and redoable manner. To do this it delegates the actual * command execution to {@link CommandHandler}s. * * Command handlers provide {@link CommandHandler#execute(ctx)} and * {@link CommandHandler#revert(ctx)} methods to un- and redo a command * identified by a command context. * * * ## Life-Cycle events * * In the process the command stack fires a number of life-cycle events * that other components to participate in the command execution. * * * preExecute * * preExecuted * * execute * * executed * * postExecute * * postExecuted * * revert * * reverted * * A special event is used for validating, whether a command can be * performed prior to its execution. * * * canExecute * * Each of the events is fired as `commandStack.{eventName}` and * `commandStack.{commandName}.{eventName}`, respectively. This gives * components fine grained control on where to hook into. * * The event object fired transports `command`, the name of the * command and `context`, the command context. * * * ## Creating Command Handlers * * Command handlers should provide the {@link CommandHandler#execute(ctx)} * and {@link CommandHandler#revert(ctx)} methods to implement * redoing and undoing of a command. * * A command handler _must_ ensure undo is performed properly in order * not to break the undo chain. It must also return the shapes that * got changed during the `execute` and `revert` operations. * * Command handlers may execute other modeling operations (and thus * commands) in their `preExecute(d)` and `postExecute(d)` phases. The command * stack will properly group all commands together into a logical unit * that may be re- and undone atomically. * * Command handlers must not execute other commands from within their * core implementation (`execute`, `revert`). * * * ## Change Tracking * * During the execution of the CommandStack it will keep track of all * elements that have been touched during the command's execution. * * At the end of the CommandStack execution it will notify interested * components via an 'elements.changed' event with all the dirty * elements. * * The event can be picked up by components that are interested in the fact * that elements have been changed. One use case for this is updating * their graphical representation after moving / resizing or deletion. * * @see CommandHandler * * @param {EventBus} eventBus * @param {Injector} injector */ export default function CommandStack(eventBus, injector) { /** * A map of all registered command handlers. * * @type {CommandHandlerMap} */ this._handlerMap = {}; /** * A stack containing all re/undoable actions on the diagram * * @type {CommandStackAction[]} */ this._stack = []; /** * The current index on the stack * * @type {number} */ this._stackIdx = -1; /** * Current active commandStack execution * * @type {CurrentExecution} */ this._currentExecution = { actions: [], dirty: [], trigger: null }; /** * @type {Injector} */ this._injector = injector; /** * @type EventBus */ this._eventBus = eventBus; /** * @type { number } */ this._uid = 1; eventBus.on([ 'diagram.destroy', 'diagram.clear' ], function() { this.clear(false); }, this); } CommandStack.$inject = [ 'eventBus', 'injector' ]; /** * Execute a command. * * @param {string} command The command to execute. * @param {CommandContext} context The context with which to execute the command. */ CommandStack.prototype.execute = function(command, context) { if (!command) { throw new Error('command required'); } this._currentExecution.trigger = 'execute'; const action = { command: command, context: context }; this._pushAction(action); this._internalExecute(action); this._popAction(); }; /** * Check whether a command can be executed. * * Implementors may hook into the mechanism on two ways: * * * in event listeners: * * Users may prevent the execution via an event listener. * It must prevent the default action for `commandStack.(<command>.)canExecute` events. * * * in command handlers: * * If the method {@link CommandHandler#canExecute} is implemented in a handler * it will be called to figure out whether the execution is allowed. * * @param {string} command The command to execute. * @param {CommandContext} context The context with which to execute the command. * * @return {boolean} Whether the command can be executed with the given context. */ CommandStack.prototype.canExecute = function(command, context) { const action = { command: command, context: context }; const handler = this._getHandler(command); let result = this._fire(command, 'canExecute', action); // handler#canExecute will only be called if no listener // decided on a result already if (result === undefined) { if (!handler) { return false; } if (handler.canExecute) { result = handler.canExecute(context); } } return result; }; /** * Clear the command stack, erasing all undo / redo history. * * @param {boolean} [emit=true] Whether to fire an event. Defaults to `true`. */ CommandStack.prototype.clear = function(emit) { this._stack.length = 0; this._stackIdx = -1; if (emit !== false) { this._fire('changed', { trigger: 'clear' }); } }; /** * Undo last command(s) */ CommandStack.prototype.undo = function() { let action = this._getUndoAction(), next; if (action) { this._currentExecution.trigger = 'undo'; this._pushAction(action); while (action) { this._internalUndo(action); next = this._getUndoAction(); if (!next || next.id !== action.id) { break; } action = next; } this._popAction(); } }; /** * Redo last command(s) */ CommandStack.prototype.redo = function() { let action = this._getRedoAction(), next; if (action) { this._currentExecution.trigger = 'redo'; this._pushAction(action); while (action) { this._internalExecute(action, true); next = this._getRedoAction(); if (!next || next.id !== action.id) { break; } action = next; } this._popAction(); } }; /** * Register a handler instance with the command stack. * * @param {string} command Command to be executed. * @param {CommandHandler} handler Handler to execute the command. */ CommandStack.prototype.register = function(command, handler) { this._setHandler(command, handler); }; /** * Register a handler type with the command stack by instantiating it and * injecting its dependencies. * * @param {string} command Command to be executed. * @param {CommandHandlerConstructor} handlerCls Constructor to instantiate a {@link CommandHandler}. */ CommandStack.prototype.registerHandler = function(command, handlerCls) { if (!command || !handlerCls) { throw new Error('command and handlerCls must be defined'); } const handler = this._injector.instantiate(handlerCls); this.register(command, handler); }; /** * @return {boolean} */ CommandStack.prototype.canUndo = function() { return !!this._getUndoAction(); }; /** * @return {boolean} */ CommandStack.prototype.canRedo = function() { return !!this._getRedoAction(); }; // stack access ////////////////////// CommandStack.prototype._getRedoAction = function() { return this._stack[this._stackIdx + 1]; }; CommandStack.prototype._getUndoAction = function() { return this._stack[this._stackIdx]; }; // internal functionality ////////////////////// CommandStack.prototype._internalUndo = function(action) { const command = action.command, context = action.context; const handler = this._getHandler(command); // guard against illegal nested command stack invocations this._atomicDo(() => { this._fire(command, 'revert', action); if (handler.revert) { this._markDirty(handler.revert(context)); } this._revertedAction(action); this._fire(command, 'reverted', action); }); }; CommandStack.prototype._fire = function(command, qualifier, event) { if (arguments.length < 3) { event = qualifier; qualifier = null; } const names = qualifier ? [ command + '.' + qualifier, qualifier ] : [ command ]; let result; event = this._eventBus.createEvent(event); for (const name of names) { result = this._eventBus.fire('commandStack.' + name, event); if (event.cancelBubble) { break; } } return result; }; CommandStack.prototype._createId = function() { return this._uid++; }; CommandStack.prototype._atomicDo = function(fn) { const execution = this._currentExecution; execution.atomic = true; try { fn(); } finally { execution.atomic = false; } }; CommandStack.prototype._internalExecute = function(action, redo) { const command = action.command, context = action.context; const handler = this._getHandler(command); if (!handler) { throw new Error('no command handler registered for <' + command + '>'); } this._pushAction(action); if (!redo) { this._fire(command, 'preExecute', action); if (handler.preExecute) { handler.preExecute(context); } this._fire(command, 'preExecuted', action); } // guard against illegal nested command stack invocations this._atomicDo(() => { this._fire(command, 'execute', action); if (handler.execute) { // actual execute + mark return results as dirty this._markDirty(handler.execute(context)); } // log to stack this._executedAction(action, redo); this._fire(command, 'executed', action); }); if (!redo) { this._fire(command, 'postExecute', action); if (handler.postExecute) { handler.postExecute(context); } this._fire(command, 'postExecuted', action); } this._popAction(); }; CommandStack.prototype._pushAction = function(action) { const execution = this._currentExecution, actions = execution.actions; const baseAction = actions[0]; if (execution.atomic) { throw new Error('illegal invocation in <execute> or <revert> phase (action: ' + action.command + ')'); } if (!action.id) { action.id = (baseAction && baseAction.id) || this._createId(); } actions.push(action); }; CommandStack.prototype._popAction = function() { const execution = this._currentExecution, trigger = execution.trigger, actions = execution.actions, dirty = execution.dirty; actions.pop(); if (!actions.length) { this._eventBus.fire('elements.changed', { elements: uniqueBy('id', dirty.reverse()) }); dirty.length = 0; this._fire('changed', { trigger: trigger }); execution.trigger = null; } }; CommandStack.prototype._markDirty = function(elements) { const execution = this._currentExecution; if (!elements) { return; } elements = isArray(elements) ? elements : [ elements ]; execution.dirty = execution.dirty.concat(elements); }; CommandStack.prototype._executedAction = function(action, redo) { const stackIdx = ++this._stackIdx; if (!redo) { this._stack.splice(stackIdx, this._stack.length, action); } }; CommandStack.prototype._revertedAction = function(action) { this._stackIdx--; }; CommandStack.prototype._getHandler = function(command) { return this._handlerMap[command]; }; CommandStack.prototype._setHandler = function(command, handler) { if (!command || !handler) { throw new Error('command and handler required'); } if (this._handlerMap[command]) { throw new Error('overriding handler for command <' + command + '>'); } this._handlerMap[command] = handler; };