552 lines
12 KiB
JavaScript
552 lines
12 KiB
JavaScript
import {
|
|
uniqueBy,
|
|
isArray
|
|
} from 'min-dash';
|
|
|
|
|
|
/**
|
|
* @typedef {import('didi').Injector} Injector
|
|
*
|
|
* @typedef {import('../core/EventBus').default} EventBus
|
|
*
|
|
* @typedef {import('./CommandStack').CommandContext} CommandContext
|
|
* @typedef {import('./CommandHandler').default} CommandHandler
|
|
* @typedef {import('./CommandStack').CommandHandlerConstructor} CommandHandlerConstructor
|
|
* @typedef {import('./CommandStack').CommandHandlerMap} CommandHandlerMap
|
|
* @typedef {import('./CommandStack').CommandStackAction} CommandStackAction
|
|
*
|
|
* @property {CommandStackAction[]} actions
|
|
* @property {Base[]} dirty
|
|
* @property {string|null} trigger - Either 'execute', 'undo', 'redo', 'clear' or null.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} CurrentExecution
|
|
* @property {CommandStackAction[]} actions
|
|
* @property {Base[]} dirty
|
|
* @property {string} trigger
|
|
*/
|
|
|
|
/**
|
|
* 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(action);
|
|
};
|
|
|
|
|
|
/**
|
|
* 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(action);
|
|
};
|
|
|
|
|
|
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;
|
|
};
|