undoRedo.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  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. var plugin = this;
  26. this.instance = instance;
  27. this.doneActions = [];
  28. this.undoneActions = [];
  29. this.ignoreNewActions = false;
  30. instance.addHook('afterChange', function (changes, source) {
  31. if (changes && source !== 'UndoRedo.undo' && source !== 'UndoRedo.redo') {
  32. plugin.done(new UndoRedo.ChangeAction(changes));
  33. }
  34. });
  35. instance.addHook('afterCreateRow', function (index, amount, source) {
  36. if (source === 'UndoRedo.undo' || source === 'UndoRedo.undo' || source === 'auto') {
  37. return;
  38. }
  39. var action = new UndoRedo.CreateRowAction(index, amount);
  40. plugin.done(action);
  41. });
  42. instance.addHook('beforeRemoveRow', function (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', function (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', function (index, amount, logicColumns, source) {
  58. if (source === 'UndoRedo.undo' || source === 'UndoRedo.redo' || source === 'auto') {
  59. return;
  60. }
  61. var originalData = plugin.instance.getSourceDataArray();
  62. index = (plugin.instance.countCols() + index) % plugin.instance.countCols();
  63. var removedData = [];
  64. var headers = [];
  65. var indexes = [];
  66. rangeEach(originalData.length - 1, function (i) {
  67. var column = [];
  68. var origRow = originalData[i];
  69. rangeEach(index, index + (amount - 1), function (j) {
  70. column.push(origRow[instance.runHooks('modifyCol', j)]);
  71. });
  72. removedData.push(column);
  73. });
  74. rangeEach(amount - 1, function (i) {
  75. indexes.push(instance.runHooks('modifyCol', index + i));
  76. });
  77. if (Array.isArray(instance.getSettings().colHeaders)) {
  78. rangeEach(amount - 1, function (i) {
  79. headers.push(instance.getSettings().colHeaders[instance.runHooks('modifyCol', index + i)] || null);
  80. });
  81. }
  82. var manualColumnMovePlugin = plugin.instance.getPlugin('manualColumnMove');
  83. var columnsMap = manualColumnMovePlugin.isEnabled() ? manualColumnMovePlugin.columnsMapper.__arrayMap : [];
  84. var action = new UndoRedo.RemoveColumnAction(index, indexes, removedData, headers, columnsMap);
  85. plugin.done(action);
  86. });
  87. instance.addHook('beforeCellAlignment', function (stateBefore, range, type, alignment) {
  88. var action = new UndoRedo.CellAlignmentAction(stateBefore, range, type, alignment);
  89. plugin.done(action);
  90. });
  91. instance.addHook('beforeFilter', function (formulaStacks) {
  92. plugin.done(new UndoRedo.FiltersAction(formulaStacks));
  93. });
  94. instance.addHook('beforeRowMove', function (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. var action = this.doneActions.pop();
  116. var actionClone = deepClone(action);
  117. var instance = this.instance;
  118. var continueAction = instance.runHooks('beforeUndo', actionClone);
  119. if (continueAction === false) {
  120. return;
  121. }
  122. this.ignoreNewActions = true;
  123. var that = this;
  124. action.undo(this.instance, function () {
  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. var action = this.undoneActions.pop();
  140. var actionClone = deepClone(action);
  141. var instance = this.instance;
  142. var continueAction = instance.runHooks('beforeRedo', actionClone);
  143. if (continueAction === false) {
  144. return;
  145. }
  146. this.ignoreNewActions = true;
  147. var that = this;
  148. action.redo(this.instance, function () {
  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. var data = deepClone(this.changes),
  198. emptyRowsAtTheEnd = instance.countEmptyRows(true),
  199. emptyColsAtTheEnd = instance.countEmptyCols(true);
  200. for (var 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 (var _i = 0, _len = data.length; _i < _len; _i++) {
  206. if (instance.getSettings().minSpareRows && data[_i][0] + 1 + instance.getSettings().minSpareRows === instance.countRows() && emptyRowsAtTheEnd == instance.getSettings().minSpareRows) {
  207. instance.alter('remove_row', parseInt(data[_i][0] + 1, 10), instance.getSettings().minSpareRows);
  208. instance.undoRedo.doneActions.pop();
  209. }
  210. if (instance.getSettings().minSpareCols && data[_i][1] + 1 + instance.getSettings().minSpareCols === instance.countCols() && emptyColsAtTheEnd == instance.getSettings().minSpareCols) {
  211. instance.alter('remove_col', parseInt(data[_i][1] + 1, 10), instance.getSettings().minSpareCols);
  212. instance.undoRedo.doneActions.pop();
  213. }
  214. }
  215. };
  216. UndoRedo.ChangeAction.prototype.redo = function (instance, onFinishCallback) {
  217. var data = deepClone(this.changes);
  218. for (var i = 0, len = data.length; i < len; i++) {
  219. data[i].splice(2, 1);
  220. }
  221. instance.addHookOnce('afterChange', onFinishCallback);
  222. instance.setDataAtRowProp(data, null, null, 'UndoRedo.redo');
  223. };
  224. /**
  225. * Create row action.
  226. */
  227. UndoRedo.CreateRowAction = function (index, amount) {
  228. this.index = index;
  229. this.amount = amount;
  230. this.actionType = 'insert_row';
  231. };
  232. inherit(UndoRedo.CreateRowAction, UndoRedo.Action);
  233. UndoRedo.CreateRowAction.prototype.undo = function (instance, undoneCallback) {
  234. var rowCount = instance.countRows(),
  235. minSpareRows = instance.getSettings().minSpareRows;
  236. if (this.index >= rowCount && this.index - minSpareRows < rowCount) {
  237. this.index -= minSpareRows; // work around the situation where the needed row was removed due to an 'undo' of a made change
  238. }
  239. instance.addHookOnce('afterRemoveRow', undoneCallback);
  240. instance.alter('remove_row', this.index, this.amount, 'UndoRedo.undo');
  241. };
  242. UndoRedo.CreateRowAction.prototype.redo = function (instance, redoneCallback) {
  243. instance.addHookOnce('afterCreateRow', redoneCallback);
  244. instance.alter('insert_row', this.index, this.amount, 'UndoRedo.redo');
  245. };
  246. /**
  247. * Remove row action.
  248. */
  249. UndoRedo.RemoveRowAction = function (index, data) {
  250. this.index = index;
  251. this.data = data;
  252. this.actionType = 'remove_row';
  253. };
  254. inherit(UndoRedo.RemoveRowAction, UndoRedo.Action);
  255. UndoRedo.RemoveRowAction.prototype.undo = function (instance, undoneCallback) {
  256. instance.alter('insert_row', this.index, this.data.length, 'UndoRedo.undo');
  257. instance.addHookOnce('afterRender', undoneCallback);
  258. instance.populateFromArray(this.index, 0, this.data, void 0, void 0, 'UndoRedo.undo');
  259. };
  260. UndoRedo.RemoveRowAction.prototype.redo = function (instance, redoneCallback) {
  261. instance.addHookOnce('afterRemoveRow', redoneCallback);
  262. instance.alter('remove_row', this.index, this.data.length, 'UndoRedo.redo');
  263. };
  264. /**
  265. * Create column action.
  266. */
  267. UndoRedo.CreateColumnAction = function (index, amount) {
  268. this.index = index;
  269. this.amount = amount;
  270. this.actionType = 'insert_col';
  271. };
  272. inherit(UndoRedo.CreateColumnAction, UndoRedo.Action);
  273. UndoRedo.CreateColumnAction.prototype.undo = function (instance, undoneCallback) {
  274. instance.addHookOnce('afterRemoveCol', undoneCallback);
  275. instance.alter('remove_col', this.index, this.amount, 'UndoRedo.undo');
  276. };
  277. UndoRedo.CreateColumnAction.prototype.redo = function (instance, redoneCallback) {
  278. instance.addHookOnce('afterCreateCol', redoneCallback);
  279. instance.alter('insert_col', this.index, this.amount, 'UndoRedo.redo');
  280. };
  281. /**
  282. * Remove column action.
  283. */
  284. UndoRedo.RemoveColumnAction = function (index, indexes, data, headers, columnPositions) {
  285. this.index = index;
  286. this.indexes = indexes;
  287. this.data = data;
  288. this.amount = this.data[0].length;
  289. this.headers = headers;
  290. this.columnPositions = columnPositions.slice(0);
  291. this.actionType = 'remove_col';
  292. };
  293. inherit(UndoRedo.RemoveColumnAction, UndoRedo.Action);
  294. UndoRedo.RemoveColumnAction.prototype.undo = function (instance, undoneCallback) {
  295. var _this = this;
  296. var row = void 0;
  297. var ascendingIndexes = this.indexes.slice(0).sort();
  298. var sortByIndexes = function sortByIndexes(elem, j, arr) {
  299. return arr[_this.indexes.indexOf(ascendingIndexes[j])];
  300. };
  301. var sortedData = [];
  302. rangeEach(this.data.length - 1, function (i) {
  303. sortedData[i] = arrayMap(_this.data[i], sortByIndexes);
  304. });
  305. var sortedHeaders = [];
  306. sortedHeaders = arrayMap(this.headers, sortByIndexes);
  307. var changes = [];
  308. // TODO: Temporary hook for undo/redo mess
  309. instance.runHooks('beforeCreateCol', this.indexes[0], this.indexes[this.indexes.length - 1], 'UndoRedo.undo');
  310. rangeEach(this.data.length - 1, function (i) {
  311. row = instance.getSourceDataAtRow(i);
  312. rangeEach(ascendingIndexes.length - 1, function (j) {
  313. row.splice(ascendingIndexes[j], 0, sortedData[i][j]);
  314. changes.push([i, ascendingIndexes[j], null, sortedData[i][j]]);
  315. });
  316. });
  317. // TODO: Temporary hook for undo/redo mess
  318. if (instance.getPlugin('formulas')) {
  319. instance.getPlugin('formulas').onAfterSetDataAtCell(changes);
  320. }
  321. if (typeof this.headers !== 'undefined') {
  322. rangeEach(sortedHeaders.length - 1, function (j) {
  323. instance.getSettings().colHeaders.splice(ascendingIndexes[j], 0, sortedHeaders[j]);
  324. });
  325. }
  326. if (instance.getPlugin('manualColumnMove')) {
  327. instance.getPlugin('manualColumnMove').columnsMapper.__arrayMap = this.columnPositions;
  328. }
  329. instance.addHookOnce('afterRender', undoneCallback);
  330. // TODO: Temporary hook for undo/redo mess
  331. instance.runHooks('afterCreateCol', this.indexes[0], this.indexes[this.indexes.length - 1], 'UndoRedo.undo');
  332. if (instance.getPlugin('formulas')) {
  333. instance.getPlugin('formulas').recalculateFull();
  334. }
  335. instance.render();
  336. };
  337. UndoRedo.RemoveColumnAction.prototype.redo = function (instance, redoneCallback) {
  338. instance.addHookOnce('afterRemoveCol', redoneCallback);
  339. instance.alter('remove_col', this.index, this.amount, 'UndoRedo.redo');
  340. };
  341. /**
  342. * Cell alignment action.
  343. */
  344. UndoRedo.CellAlignmentAction = function (stateBefore, range, type, alignment) {
  345. this.stateBefore = stateBefore;
  346. this.range = range;
  347. this.type = type;
  348. this.alignment = alignment;
  349. };
  350. UndoRedo.CellAlignmentAction.prototype.undo = function (instance, undoneCallback) {
  351. if (!instance.getPlugin('contextMenu').isEnabled()) {
  352. return;
  353. }
  354. for (var row = this.range.from.row; row <= this.range.to.row; row++) {
  355. for (var col = this.range.from.col; col <= this.range.to.col; col++) {
  356. instance.setCellMeta(row, col, 'className', this.stateBefore[row][col] || ' htLeft');
  357. }
  358. }
  359. instance.addHookOnce('afterRender', undoneCallback);
  360. instance.render();
  361. };
  362. UndoRedo.CellAlignmentAction.prototype.redo = function (instance, undoneCallback) {
  363. if (!instance.getPlugin('contextMenu').isEnabled()) {
  364. return;
  365. }
  366. instance.selectCell(this.range.from.row, this.range.from.col, this.range.to.row, this.range.to.col);
  367. instance.getPlugin('contextMenu').executeCommand('alignment:' + this.alignment.replace('ht', '').toLowerCase());
  368. instance.addHookOnce('afterRender', undoneCallback);
  369. instance.render();
  370. };
  371. /**
  372. * Filters action.
  373. */
  374. UndoRedo.FiltersAction = function (formulaStacks) {
  375. this.formulaStacks = formulaStacks;
  376. this.actionType = 'filter';
  377. };
  378. inherit(UndoRedo.FiltersAction, UndoRedo.Action);
  379. UndoRedo.FiltersAction.prototype.undo = function (instance, undoneCallback) {
  380. var filters = instance.getPlugin('filters');
  381. instance.addHookOnce('afterRender', undoneCallback);
  382. filters.formulaCollection.importAllFormulas(this.formulaStacks.slice(0, this.formulaStacks.length - 1));
  383. filters.filter();
  384. };
  385. UndoRedo.FiltersAction.prototype.redo = function (instance, redoneCallback) {
  386. var filters = instance.getPlugin('filters');
  387. instance.addHookOnce('afterRender', redoneCallback);
  388. filters.formulaCollection.importAllFormulas(this.formulaStacks);
  389. filters.filter();
  390. };
  391. /**
  392. * ManualRowMove action.
  393. * @TODO: removeRow undo should works on logical index
  394. */
  395. UndoRedo.RowMoveAction = function (movedRows, target) {
  396. this.rows = movedRows.slice();
  397. this.target = target;
  398. };
  399. inherit(UndoRedo.RowMoveAction, UndoRedo.Action);
  400. UndoRedo.RowMoveAction.prototype.undo = function (instance, undoneCallback) {
  401. var manualRowMove = instance.getPlugin('manualRowMove');
  402. instance.addHookOnce('afterRender', undoneCallback);
  403. var mod = this.rows[0] < this.target ? -1 * this.rows.length : 0;
  404. var newTarget = this.rows[0] > this.target ? this.rows[0] + this.rows.length : this.rows[0];
  405. var newRows = [];
  406. var rowsLen = this.rows.length + mod;
  407. for (var i = mod; i < rowsLen; i++) {
  408. newRows.push(this.target + i);
  409. }
  410. manualRowMove.moveRows(newRows.slice(), newTarget);
  411. instance.render();
  412. instance.selection.setRangeStartOnly(new CellCoords(this.rows[0], 0));
  413. instance.selection.setRangeEnd(new CellCoords(this.rows[this.rows.length - 1], instance.countCols() - 1));
  414. };
  415. UndoRedo.RowMoveAction.prototype.redo = function (instance, redoneCallback) {
  416. var manualRowMove = instance.getPlugin('manualRowMove');
  417. instance.addHookOnce('afterRender', redoneCallback);
  418. manualRowMove.moveRows(this.rows.slice(), this.target);
  419. instance.render();
  420. var startSelection = this.rows[0] < this.target ? this.target - this.rows.length : this.target;
  421. instance.selection.setRangeStartOnly(new CellCoords(startSelection, 0));
  422. instance.selection.setRangeEnd(new CellCoords(startSelection + this.rows.length - 1, instance.countCols() - 1));
  423. };
  424. function init() {
  425. var instance = this;
  426. var pluginEnabled = typeof instance.getSettings().undo == 'undefined' || instance.getSettings().undo;
  427. if (pluginEnabled) {
  428. if (!instance.undoRedo) {
  429. /**
  430. * Instance of Handsontable.UndoRedo Plugin {@link Handsontable.UndoRedo}
  431. *
  432. * @alias undoRedo
  433. * @memberof! Handsontable.Core#
  434. * @type {UndoRedo}
  435. */
  436. instance.undoRedo = new UndoRedo(instance);
  437. exposeUndoRedoMethods(instance);
  438. instance.addHook('beforeKeyDown', onBeforeKeyDown);
  439. instance.addHook('afterChange', onAfterChange);
  440. }
  441. } else if (instance.undoRedo) {
  442. delete instance.undoRedo;
  443. removeExposedUndoRedoMethods(instance);
  444. instance.removeHook('beforeKeyDown', onBeforeKeyDown);
  445. instance.removeHook('afterChange', onAfterChange);
  446. }
  447. }
  448. function onBeforeKeyDown(event) {
  449. var instance = this;
  450. var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey;
  451. if (ctrlDown) {
  452. if (event.keyCode === 89 || event.shiftKey && event.keyCode === 90) {
  453. // CTRL + Y or CTRL + SHIFT + Z
  454. instance.undoRedo.redo();
  455. stopImmediatePropagation(event);
  456. } else if (event.keyCode === 90) {
  457. // CTRL + Z
  458. instance.undoRedo.undo();
  459. stopImmediatePropagation(event);
  460. }
  461. }
  462. }
  463. function onAfterChange(changes, source) {
  464. var instance = this;
  465. if (source === 'loadData') {
  466. return instance.undoRedo.clear();
  467. }
  468. }
  469. function exposeUndoRedoMethods(instance) {
  470. /**
  471. * {@link UndoRedo#undo}
  472. * @alias undo
  473. * @memberof! Handsontable.Core#
  474. */
  475. instance.undo = function () {
  476. return instance.undoRedo.undo();
  477. };
  478. /**
  479. * {@link UndoRedo#redo}
  480. * @alias redo
  481. * @memberof! Handsontable.Core#
  482. */
  483. instance.redo = function () {
  484. return instance.undoRedo.redo();
  485. };
  486. /**
  487. * {@link UndoRedo#isUndoAvailable}
  488. * @alias isUndoAvailable
  489. * @memberof! Handsontable.Core#
  490. */
  491. instance.isUndoAvailable = function () {
  492. return instance.undoRedo.isUndoAvailable();
  493. };
  494. /**
  495. * {@link UndoRedo#isRedoAvailable}
  496. * @alias isRedoAvailable
  497. * @memberof! Handsontable.Core#
  498. */
  499. instance.isRedoAvailable = function () {
  500. return instance.undoRedo.isRedoAvailable();
  501. };
  502. /**
  503. * {@link UndoRedo#clear}
  504. * @alias clearUndo
  505. * @memberof! Handsontable.Core#
  506. */
  507. instance.clearUndo = function () {
  508. return instance.undoRedo.clear();
  509. };
  510. }
  511. function removeExposedUndoRedoMethods(instance) {
  512. delete instance.undo;
  513. delete instance.redo;
  514. delete instance.isUndoAvailable;
  515. delete instance.isRedoAvailable;
  516. delete instance.clearUndo;
  517. }
  518. var hook = Hooks.getSingleton();
  519. hook.add('afterInit', init);
  520. hook.add('afterUpdateSettings', init);
  521. hook.register('beforeUndo');
  522. hook.register('afterUndo');
  523. hook.register('beforeRedo');
  524. hook.register('afterRedo');