123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504 |
- import {
- uniqueBy,
- isArray
- } from 'min-dash';
- /**
- * 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` and `postExecute` 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 {Object}
- */
- this._handlerMap = {};
- /**
- * A stack containing all re/undoable actions on the diagram
- *
- * @type {Array<Object>}
- */
- this._stack = [];
- /**
- * The current index on the stack
- *
- * @type {Number}
- */
- this._stackIdx = -1;
- /**
- * Current active commandStack execution
- *
- * @type {Object}
- */
- this._currentExecution = {
- actions: [],
- dirty: []
- };
- this._injector = injector;
- this._eventBus = eventBus;
- 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 {Object} context the environment to execute the command in
- */
- CommandStack.prototype.execute = function(command, context) {
- if (!command) {
- throw new Error('command required');
- }
- var action = { command: command, context: context };
- this._pushAction(action);
- this._internalExecute(action);
- this._popAction(action);
- };
- /**
- * Ask whether a given 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 {Object} context the environment to execute the command in
- *
- * @return {Boolean} true if the command can be executed
- */
- CommandStack.prototype.canExecute = function(command, context) {
- var action = { command: command, context: context };
- var handler = this._getHandler(command);
- var 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
- */
- CommandStack.prototype.clear = function(emit) {
- this._stack.length = 0;
- this._stackIdx = -1;
- if (emit !== false) {
- this._fire('changed');
- }
- };
- /**
- * Undo last command(s)
- */
- CommandStack.prototype.undo = function() {
- var action = this._getUndoAction(),
- next;
- if (action) {
- 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() {
- var action = this._getRedoAction(),
- next;
- if (action) {
- 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
- * @param {CommandHandler} handler
- */
- 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
- * @param {Function} a constructor for a {@link CommandHandler}
- */
- CommandStack.prototype.registerHandler = function(command, handlerCls) {
- if (!command || !handlerCls) {
- throw new Error('command and handlerCls must be defined');
- }
- var handler = this._injector.instantiate(handlerCls);
- this.register(command, handler);
- };
- CommandStack.prototype.canUndo = function() {
- return !!this._getUndoAction();
- };
- 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) {
- var self = this;
- var command = action.command,
- context = action.context;
- var handler = this._getHandler(command);
- // guard against illegal nested command stack invocations
- this._atomicDo(function() {
- self._fire(command, 'revert', action);
- if (handler.revert) {
- self._markDirty(handler.revert(context));
- }
- self._revertedAction(action);
- self._fire(command, 'reverted', action);
- });
- };
- CommandStack.prototype._fire = function(command, qualifier, event) {
- if (arguments.length < 3) {
- event = qualifier;
- qualifier = null;
- }
- var names = qualifier ? [ command + '.' + qualifier, qualifier ] : [ command ],
- i, name, result;
- event = this._eventBus.createEvent(event);
- for (i = 0; (name = names[i]); i++) {
- 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) {
- var execution = this._currentExecution;
- execution.atomic = true;
- try {
- fn();
- } finally {
- execution.atomic = false;
- }
- };
- CommandStack.prototype._internalExecute = function(action, redo) {
- var self = this;
- var command = action.command,
- context = action.context;
- var 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(function() {
- self._fire(command, 'execute', action);
- if (handler.execute) {
- // actual execute + mark return results as dirty
- self._markDirty(handler.execute(context));
- }
- // log to stack
- self._executedAction(action, redo);
- self._fire(command, 'executed', action);
- });
- if (!redo) {
- this._fire(command, 'postExecute', action);
- if (handler.postExecute) {
- handler.postExecute(context);
- }
- this._fire(command, 'postExecuted', action);
- }
- this._popAction(action);
- };
- CommandStack.prototype._pushAction = function(action) {
- var execution = this._currentExecution,
- actions = execution.actions;
- var 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() {
- var execution = this._currentExecution,
- actions = execution.actions,
- dirty = execution.dirty;
- actions.pop();
- if (!actions.length) {
- this._eventBus.fire('elements.changed', { elements: uniqueBy('id', dirty) });
- dirty.length = 0;
- this._fire('changed');
- }
- };
- CommandStack.prototype._markDirty = function(elements) {
- var execution = this._currentExecution;
- if (!elements) {
- return;
- }
- elements = isArray(elements) ? elements : [ elements ];
- execution.dirty = execution.dirty.concat(elements);
- };
- CommandStack.prototype._executedAction = function(action, redo) {
- var 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;
- };
|