CommandStack.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  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. */
  105. this._currentExecution = {
  106. actions: [],
  107. dirty: []
  108. };
  109. this._injector = injector;
  110. this._eventBus = eventBus;
  111. this._uid = 1;
  112. eventBus.on([
  113. 'diagram.destroy',
  114. 'diagram.clear'
  115. ], function() {
  116. this.clear(false);
  117. }, this);
  118. }
  119. CommandStack.$inject = [ 'eventBus', 'injector' ];
  120. /**
  121. * Execute a command
  122. *
  123. * @param {String} command the command to execute
  124. * @param {Object} context the environment to execute the command in
  125. */
  126. CommandStack.prototype.execute = function(command, context) {
  127. if (!command) {
  128. throw new Error('command required');
  129. }
  130. var action = { command: command, context: context };
  131. this._pushAction(action);
  132. this._internalExecute(action);
  133. this._popAction(action);
  134. };
  135. /**
  136. * Ask whether a given command can be executed.
  137. *
  138. * Implementors may hook into the mechanism on two ways:
  139. *
  140. * * in event listeners:
  141. *
  142. * Users may prevent the execution via an event listener.
  143. * It must prevent the default action for `commandStack.(<command>.)canExecute` events.
  144. *
  145. * * in command handlers:
  146. *
  147. * If the method {@link CommandHandler#canExecute} is implemented in a handler
  148. * it will be called to figure out whether the execution is allowed.
  149. *
  150. * @param {String} command the command to execute
  151. * @param {Object} context the environment to execute the command in
  152. *
  153. * @return {Boolean} true if the command can be executed
  154. */
  155. CommandStack.prototype.canExecute = function(command, context) {
  156. var action = { command: command, context: context };
  157. var handler = this._getHandler(command);
  158. var result = this._fire(command, 'canExecute', action);
  159. // handler#canExecute will only be called if no listener
  160. // decided on a result already
  161. if (result === undefined) {
  162. if (!handler) {
  163. return false;
  164. }
  165. if (handler.canExecute) {
  166. result = handler.canExecute(context);
  167. }
  168. }
  169. return result;
  170. };
  171. /**
  172. * Clear the command stack, erasing all undo / redo history
  173. */
  174. CommandStack.prototype.clear = function(emit) {
  175. this._stack.length = 0;
  176. this._stackIdx = -1;
  177. if (emit !== false) {
  178. this._fire('changed');
  179. }
  180. };
  181. /**
  182. * Undo last command(s)
  183. */
  184. CommandStack.prototype.undo = function() {
  185. var action = this._getUndoAction(),
  186. next;
  187. if (action) {
  188. this._pushAction(action);
  189. while (action) {
  190. this._internalUndo(action);
  191. next = this._getUndoAction();
  192. if (!next || next.id !== action.id) {
  193. break;
  194. }
  195. action = next;
  196. }
  197. this._popAction();
  198. }
  199. };
  200. /**
  201. * Redo last command(s)
  202. */
  203. CommandStack.prototype.redo = function() {
  204. var action = this._getRedoAction(),
  205. next;
  206. if (action) {
  207. this._pushAction(action);
  208. while (action) {
  209. this._internalExecute(action, true);
  210. next = this._getRedoAction();
  211. if (!next || next.id !== action.id) {
  212. break;
  213. }
  214. action = next;
  215. }
  216. this._popAction();
  217. }
  218. };
  219. /**
  220. * Register a handler instance with the command stack
  221. *
  222. * @param {String} command
  223. * @param {CommandHandler} handler
  224. */
  225. CommandStack.prototype.register = function(command, handler) {
  226. this._setHandler(command, handler);
  227. };
  228. /**
  229. * Register a handler type with the command stack
  230. * by instantiating it and injecting its dependencies.
  231. *
  232. * @param {String} command
  233. * @param {Function} a constructor for a {@link CommandHandler}
  234. */
  235. CommandStack.prototype.registerHandler = function(command, handlerCls) {
  236. if (!command || !handlerCls) {
  237. throw new Error('command and handlerCls must be defined');
  238. }
  239. var handler = this._injector.instantiate(handlerCls);
  240. this.register(command, handler);
  241. };
  242. CommandStack.prototype.canUndo = function() {
  243. return !!this._getUndoAction();
  244. };
  245. CommandStack.prototype.canRedo = function() {
  246. return !!this._getRedoAction();
  247. };
  248. // stack access //////////////////////
  249. CommandStack.prototype._getRedoAction = function() {
  250. return this._stack[this._stackIdx + 1];
  251. };
  252. CommandStack.prototype._getUndoAction = function() {
  253. return this._stack[this._stackIdx];
  254. };
  255. // internal functionality //////////////////////
  256. CommandStack.prototype._internalUndo = function(action) {
  257. var self = this;
  258. var command = action.command,
  259. context = action.context;
  260. var handler = this._getHandler(command);
  261. // guard against illegal nested command stack invocations
  262. this._atomicDo(function() {
  263. self._fire(command, 'revert', action);
  264. if (handler.revert) {
  265. self._markDirty(handler.revert(context));
  266. }
  267. self._revertedAction(action);
  268. self._fire(command, 'reverted', action);
  269. });
  270. };
  271. CommandStack.prototype._fire = function(command, qualifier, event) {
  272. if (arguments.length < 3) {
  273. event = qualifier;
  274. qualifier = null;
  275. }
  276. var names = qualifier ? [ command + '.' + qualifier, qualifier ] : [ command ],
  277. i, name, result;
  278. event = this._eventBus.createEvent(event);
  279. for (i = 0; (name = names[i]); i++) {
  280. result = this._eventBus.fire('commandStack.' + name, event);
  281. if (event.cancelBubble) {
  282. break;
  283. }
  284. }
  285. return result;
  286. };
  287. CommandStack.prototype._createId = function() {
  288. return this._uid++;
  289. };
  290. CommandStack.prototype._atomicDo = function(fn) {
  291. var execution = this._currentExecution;
  292. execution.atomic = true;
  293. try {
  294. fn();
  295. } finally {
  296. execution.atomic = false;
  297. }
  298. };
  299. CommandStack.prototype._internalExecute = function(action, redo) {
  300. var self = this;
  301. var command = action.command,
  302. context = action.context;
  303. var handler = this._getHandler(command);
  304. if (!handler) {
  305. throw new Error('no command handler registered for <' + command + '>');
  306. }
  307. this._pushAction(action);
  308. if (!redo) {
  309. this._fire(command, 'preExecute', action);
  310. if (handler.preExecute) {
  311. handler.preExecute(context);
  312. }
  313. this._fire(command, 'preExecuted', action);
  314. }
  315. // guard against illegal nested command stack invocations
  316. this._atomicDo(function() {
  317. self._fire(command, 'execute', action);
  318. if (handler.execute) {
  319. // actual execute + mark return results as dirty
  320. self._markDirty(handler.execute(context));
  321. }
  322. // log to stack
  323. self._executedAction(action, redo);
  324. self._fire(command, 'executed', action);
  325. });
  326. if (!redo) {
  327. this._fire(command, 'postExecute', action);
  328. if (handler.postExecute) {
  329. handler.postExecute(context);
  330. }
  331. this._fire(command, 'postExecuted', action);
  332. }
  333. this._popAction(action);
  334. };
  335. CommandStack.prototype._pushAction = function(action) {
  336. var execution = this._currentExecution,
  337. actions = execution.actions;
  338. var baseAction = actions[0];
  339. if (execution.atomic) {
  340. throw new Error('illegal invocation in <execute> or <revert> phase (action: ' + action.command + ')');
  341. }
  342. if (!action.id) {
  343. action.id = (baseAction && baseAction.id) || this._createId();
  344. }
  345. actions.push(action);
  346. };
  347. CommandStack.prototype._popAction = function() {
  348. var execution = this._currentExecution,
  349. actions = execution.actions,
  350. dirty = execution.dirty;
  351. actions.pop();
  352. if (!actions.length) {
  353. this._eventBus.fire('elements.changed', { elements: uniqueBy('id', dirty) });
  354. dirty.length = 0;
  355. this._fire('changed');
  356. }
  357. };
  358. CommandStack.prototype._markDirty = function(elements) {
  359. var execution = this._currentExecution;
  360. if (!elements) {
  361. return;
  362. }
  363. elements = isArray(elements) ? elements : [ elements ];
  364. execution.dirty = execution.dirty.concat(elements);
  365. };
  366. CommandStack.prototype._executedAction = function(action, redo) {
  367. var stackIdx = ++this._stackIdx;
  368. if (!redo) {
  369. this._stack.splice(stackIdx, this._stack.length, action);
  370. }
  371. };
  372. CommandStack.prototype._revertedAction = function(action) {
  373. this._stackIdx--;
  374. };
  375. CommandStack.prototype._getHandler = function(command) {
  376. return this._handlerMap[command];
  377. };
  378. CommandStack.prototype._setHandler = function(command, handler) {
  379. if (!command || !handler) {
  380. throw new Error('command and handler required');
  381. }
  382. if (this._handlerMap[command]) {
  383. throw new Error('overriding handler for command <' + command + '>');
  384. }
  385. this._handlerMap[command] = handler;
  386. };