undoRedo.js 21 KB

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