CommandStack.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. import {
  2. uniqueBy,
  3. isArray
  4. } from 'min-dash';
  5. /**
  6. * A service that offers un- and redoable execution of commands.
  7. *
  8. * The command stack is responsible for executing modeling actions
  9. * in a un- and redoable manner. To do this it delegates the actual
  10. * command execution to {@link CommandHandler}s.
  11. *
  12. * Command handlers provide {@link CommandHandler#execute(ctx)} and
  13. * {@link CommandHandler#revert(ctx)} methods to un- and redo a command
  14. * identified by a command context.
  15. *
  16. *
  17. * ## Life-Cycle events
  18. *
  19. * In the process the command stack fires a number of life-cycle events
  20. * that other components to participate in the command execution.
  21. *
  22. * * preExecute
  23. * * preExecuted
  24. * * execute
  25. * * executed
  26. * * postExecute
  27. * * postExecuted
  28. * * revert
  29. * * reverted
  30. *
  31. * A special event is used for validating, whether a command can be
  32. * performed prior to its execution.
  33. *
  34. * * canExecute
  35. *
  36. * Each of the events is fired as `commandStack.{eventName}` and
  37. * `commandStack.{commandName}.{eventName}`, respectively. This gives
  38. * components fine grained control on where to hook into.
  39. *
  40. * The event object fired transports `command`, the name of the
  41. * command and `context`, the command context.
  42. *
  43. *
  44. * ## Creating Command Handlers
  45. *
  46. * Command handlers should provide the {@link CommandHandler#execute(ctx)}
  47. * and {@link CommandHandler#revert(ctx)} methods to implement
  48. * redoing and undoing of a command.
  49. *
  50. * A command handler _must_ ensure undo is performed properly in order
  51. * not to break the undo chain. It must also return the shapes that
  52. * got changed during the `execute` and `revert` operations.
  53. *
  54. * Command handlers may execute other modeling operations (and thus
  55. * commands) in their `preExecute` and `postExecute` phases. The command
  56. * stack will properly group all commands together into a logical unit
  57. * that may be re- and undone atomically.
  58. *
  59. * Command handlers must not execute other commands from within their
  60. * core implementation (`execute`, `revert`).
  61. *
  62. *
  63. * ## Change Tracking
  64. *
  65. * During the execution of the CommandStack it will keep track of all
  66. * elements that have been touched during the command's execution.
  67. *
  68. * At the end of the CommandStack execution it will notify interested
  69. * components via an 'elements.changed' event with all the dirty
  70. * elements.
  71. *
  72. * The event can be picked up by components that are interested in the fact
  73. * that elements have been changed. One use case for this is updating
  74. * their graphical representation after moving / resizing or deletion.
  75. *
  76. * @see CommandHandler
  77. *
  78. * @param {EventBus} eventBus
  79. * @param {Injector} injector
  80. */
  81. export default function CommandStack(eventBus, injector) {
  82. /**
  83. * A map of all registered command handlers.
  84. *
  85. * @type {Object}
  86. */
  87. this._handlerMap = {};
  88. /**
  89. * A stack containing all re/undoable actions on the diagram
  90. *
  91. * @type {Array<Object>}
  92. */
  93. this._stack = [];
  94. /**
  95. * The current index on the stack
  96. *
  97. * @type {number}
  98. */
  99. this._stackIdx = -1;
  100. /**
  101. * Current active commandStack execution
  102. *
  103. * @type {Object}
  104. * @property {Object[]} actions
  105. * @property {Object[]} dirty
  106. * @property { 'undo' | 'redo' | 'clear' | 'execute' | null } trigger the cause of the current excecution
  107. */
  108. this._currentExecution = {
  109. actions: [],
  110. dirty: [],
  111. trigger: null
  112. };
  113. this._injector = injector;
  114. this._eventBus = eventBus;
  115. this._uid = 1;
  116. eventBus.on([
  117. 'diagram.destroy',
  118. 'diagram.clear'
  119. ], function() {
  120. this.clear(false);
  121. }, this);
  122. }
  123. CommandStack.$inject = [ 'eventBus', 'injector' ];
  124. /**
  125. * Execute a command
  126. *
  127. * @param {string} command the command to execute
  128. * @param {Object} context the environment to execute the command in
  129. */
  130. CommandStack.prototype.execute = function(command, context) {
  131. if (!command) {
  132. throw new Error('command required');
  133. }
  134. this._currentExecution.trigger = 'execute';
  135. const action = { command: command, context: context };
  136. this._pushAction(action);
  137. this._internalExecute(action);
  138. this._popAction(action);
  139. };
  140. /**
  141. * Ask whether a given command can be executed.
  142. *
  143. * Implementors may hook into the mechanism on two ways:
  144. *
  145. * * in event listeners:
  146. *
  147. * Users may prevent the execution via an event listener.
  148. * It must prevent the default action for `commandStack.(<command>.)canExecute` events.
  149. *
  150. * * in command handlers:
  151. *
  152. * If the method {@link CommandHandler#canExecute} is implemented in a handler
  153. * it will be called to figure out whether the execution is allowed.
  154. *
  155. * @param {string} command the command to execute
  156. * @param {Object} context the environment to execute the command in
  157. *
  158. * @return {boolean} true if the command can be executed
  159. */
  160. CommandStack.prototype.canExecute = function(command, context) {
  161. const action = { command: command, context: context };
  162. const handler = this._getHandler(command);
  163. let result = this._fire(command, 'canExecute', action);
  164. // handler#canExecute will only be called if no listener
  165. // decided on a result already
  166. if (result === undefined) {
  167. if (!handler) {
  168. return false;
  169. }
  170. if (handler.canExecute) {
  171. result = handler.canExecute(context);
  172. }
  173. }
  174. return result;
  175. };
  176. /**
  177. * Clear the command stack, erasing all undo / redo history
  178. */
  179. CommandStack.prototype.clear = function(emit) {
  180. this._stack.length = 0;
  181. this._stackIdx = -1;
  182. if (emit !== false) {
  183. this._fire('changed', { trigger: 'clear' });
  184. }
  185. };
  186. /**
  187. * Undo last command(s)
  188. */
  189. CommandStack.prototype.undo = function() {
  190. let action = this._getUndoAction(),
  191. next;
  192. if (action) {
  193. this._currentExecution.trigger = 'undo';
  194. this._pushAction(action);
  195. while (action) {
  196. this._internalUndo(action);
  197. next = this._getUndoAction();
  198. if (!next || next.id !== action.id) {
  199. break;
  200. }
  201. action = next;
  202. }
  203. this._popAction();
  204. }
  205. };
  206. /**
  207. * Redo last command(s)
  208. */
  209. CommandStack.prototype.redo = function() {
  210. let action = this._getRedoAction(),
  211. next;
  212. if (action) {
  213. this._currentExecution.trigger = 'redo';
  214. this._pushAction(action);
  215. while (action) {
  216. this._internalExecute(action, true);
  217. next = this._getRedoAction();
  218. if (!next || next.id !== action.id) {
  219. break;
  220. }
  221. action = next;
  222. }
  223. this._popAction();
  224. }
  225. };
  226. /**
  227. * Register a handler instance with the command stack
  228. *
  229. * @param {string} command
  230. * @param {CommandHandler} handler
  231. */
  232. CommandStack.prototype.register = function(command, handler) {
  233. this._setHandler(command, handler);
  234. };
  235. /**
  236. * Register a handler type with the command stack
  237. * by instantiating it and injecting its dependencies.
  238. *
  239. * @param {string} command
  240. * @param {Function} a constructor for a {@link CommandHandler}
  241. */
  242. CommandStack.prototype.registerHandler = function(command, handlerCls) {
  243. if (!command || !handlerCls) {
  244. throw new Error('command and handlerCls must be defined');
  245. }
  246. const handler = this._injector.instantiate(handlerCls);
  247. this.register(command, handler);
  248. };
  249. CommandStack.prototype.canUndo = function() {
  250. return !!this._getUndoAction();
  251. };
  252. CommandStack.prototype.canRedo = function() {
  253. return !!this._getRedoAction();
  254. };
  255. // stack access //////////////////////
  256. CommandStack.prototype._getRedoAction = function() {
  257. return this._stack[this._stackIdx + 1];
  258. };
  259. CommandStack.prototype._getUndoAction = function() {
  260. return this._stack[this._stackIdx];
  261. };
  262. // internal functionality //////////////////////
  263. CommandStack.prototype._internalUndo = function(action) {
  264. const command = action.command,
  265. context = action.context;
  266. const handler = this._getHandler(command);
  267. // guard against illegal nested command stack invocations
  268. this._atomicDo(() => {
  269. this._fire(command, 'revert', action);
  270. if (handler.revert) {
  271. this._markDirty(handler.revert(context));
  272. }
  273. this._revertedAction(action);
  274. this._fire(command, 'reverted', action);
  275. });
  276. };
  277. CommandStack.prototype._fire = function(command, qualifier, event) {
  278. if (arguments.length < 3) {
  279. event = qualifier;
  280. qualifier = null;
  281. }
  282. const names = qualifier ? [ command + '.' + qualifier, qualifier ] : [ command ];
  283. let result;
  284. event = this._eventBus.createEvent(event);
  285. for (const name of names) {
  286. result = this._eventBus.fire('commandStack.' + name, event);
  287. if (event.cancelBubble) {
  288. break;
  289. }
  290. }
  291. return result;
  292. };
  293. CommandStack.prototype._createId = function() {
  294. return this._uid++;
  295. };
  296. CommandStack.prototype._atomicDo = function(fn) {
  297. const execution = this._currentExecution;
  298. execution.atomic = true;
  299. try {
  300. fn();
  301. } finally {
  302. execution.atomic = false;
  303. }
  304. };
  305. CommandStack.prototype._internalExecute = function(action, redo) {
  306. const command = action.command,
  307. context = action.context;
  308. const handler = this._getHandler(command);
  309. if (!handler) {
  310. throw new Error('no command handler registered for <' + command + '>');
  311. }
  312. this._pushAction(action);
  313. if (!redo) {
  314. this._fire(command, 'preExecute', action);
  315. if (handler.preExecute) {
  316. handler.preExecute(context);
  317. }
  318. this._fire(command, 'preExecuted', action);
  319. }
  320. // guard against illegal nested command stack invocations
  321. this._atomicDo(() => {
  322. this._fire(command, 'execute', action);
  323. if (handler.execute) {
  324. // actual execute + mark return results as dirty
  325. this._markDirty(handler.execute(context));
  326. }
  327. // log to stack
  328. this._executedAction(action, redo);
  329. this._fire(command, 'executed', action);
  330. });
  331. if (!redo) {
  332. this._fire(command, 'postExecute', action);
  333. if (handler.postExecute) {
  334. handler.postExecute(context);
  335. }
  336. this._fire(command, 'postExecuted', action);
  337. }
  338. this._popAction(action);
  339. };
  340. CommandStack.prototype._pushAction = function(action) {
  341. const execution = this._currentExecution,
  342. actions = execution.actions;
  343. const baseAction = actions[0];
  344. if (execution.atomic) {
  345. throw new Error('illegal invocation in <execute> or <revert> phase (action: ' + action.command + ')');
  346. }
  347. if (!action.id) {
  348. action.id = (baseAction && baseAction.id) || this._createId();
  349. }
  350. actions.push(action);
  351. };
  352. CommandStack.prototype._popAction = function() {
  353. const execution = this._currentExecution,
  354. trigger = execution.trigger,
  355. actions = execution.actions,
  356. dirty = execution.dirty;
  357. actions.pop();
  358. if (!actions.length) {
  359. this._eventBus.fire('elements.changed', { elements: uniqueBy('id', dirty.reverse()) });
  360. dirty.length = 0;
  361. this._fire('changed', { trigger: trigger });
  362. execution.trigger = null;
  363. }
  364. };
  365. CommandStack.prototype._markDirty = function(elements) {
  366. const execution = this._currentExecution;
  367. if (!elements) {
  368. return;
  369. }
  370. elements = isArray(elements) ? elements : [ elements ];
  371. execution.dirty = execution.dirty.concat(elements);
  372. };
  373. CommandStack.prototype._executedAction = function(action, redo) {
  374. const stackIdx = ++this._stackIdx;
  375. if (!redo) {
  376. this._stack.splice(stackIdx, this._stack.length, action);
  377. }
  378. };
  379. CommandStack.prototype._revertedAction = function(action) {
  380. this._stackIdx--;
  381. };
  382. CommandStack.prototype._getHandler = function(command) {
  383. return this._handlerMap[command];
  384. };
  385. CommandStack.prototype._setHandler = function(command, handler) {
  386. if (!command || !handler) {
  387. throw new Error('command and handler required');
  388. }
  389. if (this._handlerMap[command]) {
  390. throw new Error('overriding handler for command <' + command + '>');
  391. }
  392. this._handlerMap[command] = handler;
  393. };