509e795014892fe78feffe187be845510dd582d6e7acc2bfacaf0bd2645710b6e61126fe59cd2d8919ab4a062453ee534f4e04fde3827858235c752dfffc95 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. /**
  2. * Handsontable UndoRedo class
  3. */
  4. import Hooks from './../../pluginHooks';
  5. import {arrayMap} from './../../helpers/array';
  6. import {rangeEach} from './../../helpers/number';
  7. import {inherit, deepClone} from './../../helpers/object';
  8. import {stopImmediatePropagation} from './../../helpers/dom/event';
  9. import {CellCoords} from './../../3rdparty/walkontable/src';
  10. /**
  11. * @description
  12. * Handsontable UndoRedo plugin. It allows to undo and redo certain actions done in the table.
  13. * Please note, that not all actions are currently undo-able.
  14. *
  15. * @example
  16. * ```js
  17. * ...
  18. * undo: true
  19. * ...
  20. * ```
  21. * @class UndoRedo
  22. * @plugin UndoRedo
  23. */
  24. function UndoRedo(instance) {
  25. let plugin = this;
  26. this.instance = instance;
  27. this.doneActions = [];
  28. this.undoneActions = [];
  29. this.ignoreNewActions = false;
  30. instance.addHook('afterChange', (changes, source) => {
  31. if (changes && source !== 'UndoRedo.undo' && source !== 'UndoRedo.redo') {
  32. plugin.done(new UndoRedo.ChangeAction(changes));
  33. }
  34. });
  35. instance.addHook('afterCreateRow', (index, amount, source) => {
  36. if (source === 'UndoRedo.undo' || source === 'UndoRedo.undo' || source === 'auto') {
  37. return;
  38. }
  39. let action = new UndoRedo.CreateRowAction(index, amount);
  40. plugin.done(action);
  41. });
  42. instance.addHook('beforeRemoveRow', (index, amount, logicRows, source) => {
  43. if (source === 'UndoRedo.undo' || source === 'UndoRedo.redo' || source === 'auto') {
  44. return;
  45. }
  46. var originalData = plugin.instance.getSourceDataArray();
  47. index = (originalData.length + index) % originalData.length;
  48. var removedData = deepClone(originalData.slice(index, index + amount));
  49. plugin.done(new UndoRedo.RemoveRowAction(index, removedData));
  50. });
  51. instance.addHook('afterCreateCol', (index, amount, source) => {
  52. if (source === 'UndoRedo.undo' || source === 'UndoRedo.redo' || source === 'auto') {
  53. return;
  54. }
  55. plugin.done(new UndoRedo.CreateColumnAction(index, amount));
  56. });
  57. instance.addHook('beforeRemoveCol', (index, amount, logicColumns, source) => {
  58. if (source === 'UndoRedo.undo' || source === 'UndoRedo.redo' || source === 'auto') {
  59. return;
  60. }
  61. let originalData = plugin.instance.getSourceDataArray();
  62. index = (plugin.instance.countCols() + index) % plugin.instance.countCols();
  63. let removedData = [];
  64. let headers = [];
  65. let indexes = [];
  66. rangeEach(originalData.length - 1, (i) => {
  67. let column = [];
  68. let origRow = originalData[i];
  69. rangeEach(index, index + (amount - 1), (j) => {
  70. column.push(origRow[instance.runHooks('modifyCol', j)]);
  71. });
  72. removedData.push(column);
  73. });
  74. rangeEach(amount - 1, (i) => {
  75. indexes.push(instance.runHooks('modifyCol', index + i));
  76. });
  77. if (Array.isArray(instance.getSettings().colHeaders)) {
  78. rangeEach(amount - 1, (i) => {
  79. headers.push(instance.getSettings().colHeaders[instance.runHooks('modifyCol', index + i)] || null);
  80. });
  81. }
  82. let manualColumnMovePlugin = plugin.instance.getPlugin('manualColumnMove');
  83. let columnsMap = manualColumnMovePlugin.isEnabled() ? manualColumnMovePlugin.columnsMapper.__arrayMap : [];
  84. let action = new UndoRedo.RemoveColumnAction(index, indexes, removedData, headers, columnsMap);
  85. plugin.done(action);
  86. });
  87. instance.addHook('beforeCellAlignment', (stateBefore, range, type, alignment) => {
  88. let action = new UndoRedo.CellAlignmentAction(stateBefore, range, type, alignment);
  89. plugin.done(action);
  90. });
  91. instance.addHook('beforeFilter', (formulaStacks) => {
  92. plugin.done(new UndoRedo.FiltersAction(formulaStacks));
  93. });
  94. instance.addHook('beforeRowMove', (movedRows, target) => {
  95. if (movedRows === false) {
  96. return;
  97. }
  98. plugin.done(new UndoRedo.RowMoveAction(movedRows, target));
  99. });
  100. };
  101. UndoRedo.prototype.done = function(action) {
  102. if (!this.ignoreNewActions) {
  103. this.doneActions.push(action);
  104. this.undoneActions.length = 0;
  105. }
  106. };
  107. /**
  108. * Undo last edit.
  109. *
  110. * @function undo
  111. * @memberof UndoRedo#
  112. */
  113. UndoRedo.prototype.undo = function() {
  114. if (this.isUndoAvailable()) {
  115. let action = this.doneActions.pop();
  116. let actionClone = deepClone(action);
  117. let instance = this.instance;
  118. let continueAction = instance.runHooks('beforeUndo', actionClone);
  119. if (continueAction === false) {
  120. return;
  121. }
  122. this.ignoreNewActions = true;
  123. let that = this;
  124. action.undo(this.instance, () => {
  125. that.ignoreNewActions = false;
  126. that.undoneActions.push(action);
  127. });
  128. instance.runHooks('afterUndo', actionClone);
  129. }
  130. };
  131. /**
  132. * Redo edit (used to reverse an undo).
  133. *
  134. * @function redo
  135. * @memberof UndoRedo#
  136. */
  137. UndoRedo.prototype.redo = function() {
  138. if (this.isRedoAvailable()) {
  139. let action = this.undoneActions.pop();
  140. let actionClone = deepClone(action);
  141. let instance = this.instance;
  142. let continueAction = instance.runHooks('beforeRedo', actionClone);
  143. if (continueAction === false) {
  144. return;
  145. }
  146. this.ignoreNewActions = true;
  147. let that = this;
  148. action.redo(this.instance, () => {
  149. that.ignoreNewActions = false;
  150. that.doneActions.push(action);
  151. });
  152. instance.runHooks('afterRedo', actionClone);
  153. }
  154. };
  155. /**
  156. * Check if undo action is available.
  157. *
  158. * @function isUndoAvailable
  159. * @memberof UndoRedo#
  160. * @return {Boolean} Return `true` if undo can be performed, `false` otherwise
  161. */
  162. UndoRedo.prototype.isUndoAvailable = function() {
  163. return this.doneActions.length > 0;
  164. };
  165. /**
  166. * Check if redo action is available.
  167. *
  168. * @function isRedoAvailable
  169. * @memberof UndoRedo#
  170. * @return {Boolean} Return `true` if redo can be performed, `false` otherwise.
  171. */
  172. UndoRedo.prototype.isRedoAvailable = function() {
  173. return this.undoneActions.length > 0;
  174. };
  175. /**
  176. * Clears undo history.
  177. *
  178. * @function clear
  179. * @memberof UndoRedo#
  180. */
  181. UndoRedo.prototype.clear = function() {
  182. this.doneActions.length = 0;
  183. this.undoneActions.length = 0;
  184. };
  185. UndoRedo.Action = function() {};
  186. UndoRedo.Action.prototype.undo = function() {};
  187. UndoRedo.Action.prototype.redo = function() {};
  188. /**
  189. * Change action.
  190. */
  191. UndoRedo.ChangeAction = function(changes) {
  192. this.changes = changes;
  193. this.actionType = 'change';
  194. };
  195. inherit(UndoRedo.ChangeAction, UndoRedo.Action);
  196. UndoRedo.ChangeAction.prototype.undo = function(instance, undoneCallback) {
  197. let data = deepClone(this.changes),
  198. emptyRowsAtTheEnd = instance.countEmptyRows(true),
  199. emptyColsAtTheEnd = instance.countEmptyCols(true);
  200. for (let i = 0, len = data.length; i < len; i++) {
  201. data[i].splice(3, 1);
  202. }
  203. instance.addHookOnce('afterChange', undoneCallback);
  204. instance.setDataAtRowProp(data, null, null, 'UndoRedo.undo');
  205. for (let i = 0, len = data.length; i < len; i++) {
  206. if (instance.getSettings().minSpareRows && data[i][0] + 1 + instance.getSettings().minSpareRows === instance.countRows() &&
  207. emptyRowsAtTheEnd == instance.getSettings().minSpareRows) {
  208. instance.alter('remove_row', parseInt(data[i][0] + 1, 10), instance.getSettings().minSpareRows);
  209. instance.undoRedo.doneActions.pop();
  210. }
  211. if (instance.getSettings().minSpareCols && data[i][1] + 1 + instance.getSettings().minSpareCols === instance.countCols() &&
  212. emptyColsAtTheEnd == instance.getSettings().minSpareCols) {
  213. instance.alter('remove_col', parseInt(data[i][1] + 1, 10), instance.getSettings().minSpareCols);
  214. instance.undoRedo.doneActions.pop();
  215. }
  216. }
  217. };
  218. UndoRedo.ChangeAction.prototype.redo = function(instance, onFinishCallback) {
  219. let data = deepClone(this.changes);
  220. for (let i = 0, len = data.length; i < len; i++) {
  221. data[i].splice(2, 1);
  222. }
  223. instance.addHookOnce('afterChange', onFinishCallback);
  224. instance.setDataAtRowProp(data, null, null, 'UndoRedo.redo');
  225. };
  226. /**
  227. * Create row action.
  228. */
  229. UndoRedo.CreateRowAction = function(index, amount) {
  230. this.index = index;
  231. this.amount = amount;
  232. this.actionType = 'insert_row';
  233. };
  234. inherit(UndoRedo.CreateRowAction, UndoRedo.Action);
  235. UndoRedo.CreateRowAction.prototype.undo = function(instance, undoneCallback) {
  236. let rowCount = instance.countRows(),
  237. minSpareRows = instance.getSettings().minSpareRows;
  238. if (this.index >= rowCount && this.index - minSpareRows < rowCount) {
  239. this.index -= minSpareRows; // work around the situation where the needed row was removed due to an 'undo' of a made change
  240. }
  241. instance.addHookOnce('afterRemoveRow', undoneCallback);
  242. instance.alter('remove_row', this.index, this.amount, 'UndoRedo.undo');
  243. };
  244. UndoRedo.CreateRowAction.prototype.redo = function(instance, redoneCallback) {
  245. instance.addHookOnce('afterCreateRow', redoneCallback);
  246. instance.alter('insert_row', this.index, this.amount, 'UndoRedo.redo');
  247. };
  248. /**
  249. * Remove row action.
  250. */
  251. UndoRedo.RemoveRowAction = function(index, data) {
  252. this.index = index;
  253. this.data = data;
  254. this.actionType = 'remove_row';
  255. };
  256. inherit(UndoRedo.RemoveRowAction, UndoRedo.Action);
  257. UndoRedo.RemoveRowAction.prototype.undo = function(instance, undoneCallback) {
  258. instance.alter('insert_row', this.index, this.data.length, 'UndoRedo.undo');
  259. instance.addHookOnce('afterRender', undoneCallback);
  260. instance.populateFromArray(this.index, 0, this.data, void 0, void 0, 'UndoRedo.undo');
  261. };
  262. UndoRedo.RemoveRowAction.prototype.redo = function(instance, redoneCallback) {
  263. instance.addHookOnce('afterRemoveRow', redoneCallback);
  264. instance.alter('remove_row', this.index, this.data.length, 'UndoRedo.redo');
  265. };
  266. /**
  267. * Create column action.
  268. */
  269. UndoRedo.CreateColumnAction = function(index, amount) {
  270. this.index = index;
  271. this.amount = amount;
  272. this.actionType = 'insert_col';
  273. };
  274. inherit(UndoRedo.CreateColumnAction, UndoRedo.Action);
  275. UndoRedo.CreateColumnAction.prototype.undo = function(instance, undoneCallback) {
  276. instance.addHookOnce('afterRemoveCol', undoneCallback);
  277. instance.alter('remove_col', this.index, this.amount, 'UndoRedo.undo');
  278. };
  279. UndoRedo.CreateColumnAction.prototype.redo = function(instance, redoneCallback) {
  280. instance.addHookOnce('afterCreateCol', redoneCallback);
  281. instance.alter('insert_col', this.index, this.amount, 'UndoRedo.redo');
  282. };
  283. /**
  284. * Remove column action.
  285. */
  286. UndoRedo.RemoveColumnAction = function(index, indexes, data, headers, columnPositions) {
  287. this.index = index;
  288. this.indexes = indexes;
  289. this.data = data;
  290. this.amount = this.data[0].length;
  291. this.headers = headers;
  292. this.columnPositions = columnPositions.slice(0);
  293. this.actionType = 'remove_col';
  294. };
  295. inherit(UndoRedo.RemoveColumnAction, UndoRedo.Action);
  296. UndoRedo.RemoveColumnAction.prototype.undo = function(instance, undoneCallback) {
  297. let row;
  298. let ascendingIndexes = this.indexes.slice(0).sort();
  299. let sortByIndexes = (elem, j, arr) => arr[this.indexes.indexOf(ascendingIndexes[j])];
  300. let sortedData = [];
  301. rangeEach(this.data.length - 1, (i) => {
  302. sortedData[i] = arrayMap(this.data[i], sortByIndexes);
  303. });
  304. let sortedHeaders = [];
  305. sortedHeaders = arrayMap(this.headers, sortByIndexes);
  306. var changes = [];
  307. // TODO: Temporary hook for undo/redo mess
  308. instance.runHooks('beforeCreateCol', this.indexes[0], this.indexes[this.indexes.length - 1], 'UndoRedo.undo');
  309. rangeEach(this.data.length - 1, (i) => {
  310. row = instance.getSourceDataAtRow(i);
  311. rangeEach(ascendingIndexes.length - 1, (j) => {
  312. row.splice(ascendingIndexes[j], 0, sortedData[i][j]);
  313. changes.push([i, ascendingIndexes[j], null, sortedData[i][j]]);
  314. });
  315. });
  316. // TODO: Temporary hook for undo/redo mess
  317. if (instance.getPlugin('formulas')) {
  318. instance.getPlugin('formulas').onAfterSetDataAtCell(changes);
  319. }
  320. if (typeof this.headers !== 'undefined') {
  321. rangeEach(sortedHeaders.length - 1, (j) => {
  322. instance.getSettings().colHeaders.splice(ascendingIndexes[j], 0, sortedHeaders[j]);
  323. });
  324. }
  325. if (instance.getPlugin('manualColumnMove')) {
  326. instance.getPlugin('manualColumnMove').columnsMapper.__arrayMap = this.columnPositions;
  327. }
  328. instance.addHookOnce('afterRender', undoneCallback);
  329. // TODO: Temporary hook for undo/redo mess
  330. instance.runHooks('afterCreateCol', this.indexes[0], this.indexes[this.indexes.length - 1], 'UndoRedo.undo');
  331. if (instance.getPlugin('formulas')) {
  332. instance.getPlugin('formulas').recalculateFull();
  333. }
  334. instance.render();
  335. };
  336. UndoRedo.RemoveColumnAction.prototype.redo = function(instance, redoneCallback) {
  337. instance.addHookOnce('afterRemoveCol', redoneCallback);
  338. instance.alter('remove_col', this.index, this.amount, 'UndoRedo.redo');
  339. };
  340. /**
  341. * Cell alignment action.
  342. */
  343. UndoRedo.CellAlignmentAction = function(stateBefore, range, type, alignment) {
  344. this.stateBefore = stateBefore;
  345. this.range = range;
  346. this.type = type;
  347. this.alignment = alignment;
  348. };
  349. UndoRedo.CellAlignmentAction.prototype.undo = function(instance, undoneCallback) {
  350. if (!instance.getPlugin('contextMenu').isEnabled()) {
  351. return;
  352. }
  353. for (var row = this.range.from.row; row <= this.range.to.row; row++) {
  354. for (var col = this.range.from.col; col <= this.range.to.col; col++) {
  355. instance.setCellMeta(row, col, 'className', this.stateBefore[row][col] || ' htLeft');
  356. }
  357. }
  358. instance.addHookOnce('afterRender', undoneCallback);
  359. instance.render();
  360. };
  361. UndoRedo.CellAlignmentAction.prototype.redo = function(instance, undoneCallback) {
  362. if (!instance.getPlugin('contextMenu').isEnabled()) {
  363. return;
  364. }
  365. instance.selectCell(this.range.from.row, this.range.from.col, this.range.to.row, this.range.to.col);
  366. instance.getPlugin('contextMenu').executeCommand(`alignment:${this.alignment.replace('ht', '').toLowerCase()}`);
  367. instance.addHookOnce('afterRender', undoneCallback);
  368. instance.render();
  369. };
  370. /**
  371. * Filters action.
  372. */
  373. UndoRedo.FiltersAction = function(formulaStacks) {
  374. this.formulaStacks = formulaStacks;
  375. this.actionType = 'filter';
  376. };
  377. inherit(UndoRedo.FiltersAction, UndoRedo.Action);
  378. UndoRedo.FiltersAction.prototype.undo = function(instance, undoneCallback) {
  379. let filters = instance.getPlugin('filters');
  380. instance.addHookOnce('afterRender', undoneCallback);
  381. filters.formulaCollection.importAllFormulas(this.formulaStacks.slice(0, this.formulaStacks.length - 1));
  382. filters.filter();
  383. };
  384. UndoRedo.FiltersAction.prototype.redo = function(instance, redoneCallback) {
  385. let filters = instance.getPlugin('filters');
  386. instance.addHookOnce('afterRender', redoneCallback);
  387. filters.formulaCollection.importAllFormulas(this.formulaStacks);
  388. filters.filter();
  389. };
  390. /**
  391. * ManualRowMove action.
  392. * @TODO: removeRow undo should works on logical index
  393. */
  394. UndoRedo.RowMoveAction = function(movedRows, target) {
  395. this.rows = movedRows.slice();
  396. this.target = target;
  397. };
  398. inherit(UndoRedo.RowMoveAction, UndoRedo.Action);
  399. UndoRedo.RowMoveAction.prototype.undo = function(instance, undoneCallback) {
  400. let manualRowMove = instance.getPlugin('manualRowMove');
  401. instance.addHookOnce('afterRender', undoneCallback);
  402. let mod = this.rows[0] < this.target ? -1 * this.rows.length : 0;
  403. let newTarget = this.rows[0] > this.target ? this.rows[0] + this.rows.length : this.rows[0];
  404. let newRows = [];
  405. let rowsLen = this.rows.length + mod;
  406. for (let i = mod; i < rowsLen; i++) {
  407. newRows.push(this.target + i);
  408. }
  409. manualRowMove.moveRows(newRows.slice(), newTarget);
  410. instance.render();
  411. instance.selection.setRangeStartOnly(new CellCoords(this.rows[0], 0));
  412. instance.selection.setRangeEnd(new CellCoords(this.rows[this.rows.length - 1], instance.countCols() - 1));
  413. };
  414. UndoRedo.RowMoveAction.prototype.redo = function(instance, redoneCallback) {
  415. let manualRowMove = instance.getPlugin('manualRowMove');
  416. instance.addHookOnce('afterRender', redoneCallback);
  417. manualRowMove.moveRows(this.rows.slice(), this.target);
  418. instance.render();
  419. let startSelection = this.rows[0] < this.target ? this.target - this.rows.length : this.target;
  420. instance.selection.setRangeStartOnly(new CellCoords(startSelection, 0));
  421. instance.selection.setRangeEnd(new CellCoords(startSelection + this.rows.length - 1, instance.countCols() - 1));
  422. };
  423. function init() {
  424. let instance = this;
  425. let pluginEnabled = typeof instance.getSettings().undo == 'undefined' || instance.getSettings().undo;
  426. if (pluginEnabled) {
  427. if (!instance.undoRedo) {
  428. /**
  429. * Instance of Handsontable.UndoRedo Plugin {@link Handsontable.UndoRedo}
  430. *
  431. * @alias undoRedo
  432. * @memberof! Handsontable.Core#
  433. * @type {UndoRedo}
  434. */
  435. instance.undoRedo = new UndoRedo(instance);
  436. exposeUndoRedoMethods(instance);
  437. instance.addHook('beforeKeyDown', onBeforeKeyDown);
  438. instance.addHook('afterChange', onAfterChange);
  439. }
  440. } else if (instance.undoRedo) {
  441. delete instance.undoRedo;
  442. removeExposedUndoRedoMethods(instance);
  443. instance.removeHook('beforeKeyDown', onBeforeKeyDown);
  444. instance.removeHook('afterChange', onAfterChange);
  445. }
  446. }
  447. function onBeforeKeyDown(event) {
  448. let instance = this;
  449. let ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey;
  450. if (ctrlDown) {
  451. if (event.keyCode === 89 || (event.shiftKey && event.keyCode === 90)) { // CTRL + Y or CTRL + SHIFT + Z
  452. instance.undoRedo.redo();
  453. stopImmediatePropagation(event);
  454. } else if (event.keyCode === 90) { // CTRL + Z
  455. instance.undoRedo.undo();
  456. stopImmediatePropagation(event);
  457. }
  458. }
  459. }
  460. function onAfterChange(changes, source) {
  461. let instance = this;
  462. if (source === 'loadData') {
  463. return instance.undoRedo.clear();
  464. }
  465. }
  466. function exposeUndoRedoMethods(instance) {
  467. /**
  468. * {@link UndoRedo#undo}
  469. * @alias undo
  470. * @memberof! Handsontable.Core#
  471. */
  472. instance.undo = function() {
  473. return instance.undoRedo.undo();
  474. };
  475. /**
  476. * {@link UndoRedo#redo}
  477. * @alias redo
  478. * @memberof! Handsontable.Core#
  479. */
  480. instance.redo = function() {
  481. return instance.undoRedo.redo();
  482. };
  483. /**
  484. * {@link UndoRedo#isUndoAvailable}
  485. * @alias isUndoAvailable
  486. * @memberof! Handsontable.Core#
  487. */
  488. instance.isUndoAvailable = function() {
  489. return instance.undoRedo.isUndoAvailable();
  490. };
  491. /**
  492. * {@link UndoRedo#isRedoAvailable}
  493. * @alias isRedoAvailable
  494. * @memberof! Handsontable.Core#
  495. */
  496. instance.isRedoAvailable = function() {
  497. return instance.undoRedo.isRedoAvailable();
  498. };
  499. /**
  500. * {@link UndoRedo#clear}
  501. * @alias clearUndo
  502. * @memberof! Handsontable.Core#
  503. */
  504. instance.clearUndo = function() {
  505. return instance.undoRedo.clear();
  506. };
  507. }
  508. function removeExposedUndoRedoMethods(instance) {
  509. delete instance.undo;
  510. delete instance.redo;
  511. delete instance.isUndoAvailable;
  512. delete instance.isRedoAvailable;
  513. delete instance.clearUndo;
  514. }
  515. const hook = Hooks.getSingleton();
  516. hook.add('afterInit', init);
  517. hook.add('afterUpdateSettings', init);
  518. hook.register('beforeUndo');
  519. hook.register('afterUndo');
  520. hook.register('beforeRedo');
  521. hook.register('afterRedo');