undocommand.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import BaseCommand from './basecommand.js';
  6. import { transformDelta, transformRangesByDeltas } from './basecommand.js';
  7. /**
  8. * The undo command stores {@link engine.model.Batch batches} applied to the {@link engine.model.Document document}
  9. * and is able to undo a batch by reversing it and transforming by other batches from {@link engine.model.Document#history history}
  10. * that happened after the reversed batch.
  11. *
  12. * The undo command also takes care of restoring the {@link engine.model.Document#selection selection} to the state before the
  13. * undone batch was applied.
  14. *
  15. * @memberOf undo
  16. * @extends undo.BaseCommand
  17. */
  18. export default class UndoCommand extends BaseCommand {
  19. /**
  20. * Executes the command. This method reverts a {@link engine.model.Batch batch} added to the command's stack, transforms
  21. * and applies the reverted version on the {@link engine.model.Document document} and removes the batch from the stack.
  22. * Then, it restores the {@link engine.model.Document#selection document selection}.
  23. *
  24. * @protected
  25. * @fires undo.UndoCommand#event:revert
  26. * @param {engine.model.Batch} [batch] A batch that should be undone. If not set, the last added batch will be undone.
  27. */
  28. _doExecute( batch = null ) {
  29. // If batch is not given, set `batchIndex` to the last index in command stack.
  30. let batchIndex = batch ? this._stack.findIndex( ( a ) => a.batch == batch ) : this._stack.length - 1;
  31. const item = this._stack.splice( batchIndex, 1 )[ 0 ];
  32. // All changes has to be done in one `enqueueChanges` callback so other listeners will not
  33. // step between consecutive deltas, or won't do changes to the document before selection is properly restored.
  34. this.editor.document.enqueueChanges( () => {
  35. const undoingBatch = this._undo( item.batch );
  36. const deltas = this.editor.document.history.getDeltas( item.batch.baseVersion );
  37. this._restoreSelection( item.selection.ranges, item.selection.isBackward, deltas );
  38. this.fire( 'revert', item.batch, undoingBatch );
  39. } );
  40. this.refreshState();
  41. }
  42. /**
  43. * Returns an index in {@link undo.BaseCommand#_stack} pointing to the item that is storing a batch that has a given
  44. * {@link engine.model.Batch#baseVersion}.
  45. *
  46. * @private
  47. * @param {Number} baseVersion The base version of the batch to find.
  48. * @returns {Number|null}
  49. */
  50. _getItemIndexFromBaseVersion( baseVersion ) {
  51. for ( let i = 0; i < this._stack.length; i++ ) {
  52. if ( this._stack[ i ].batch.baseVersion == baseVersion ) {
  53. return i;
  54. }
  55. }
  56. return null;
  57. }
  58. /**
  59. * Undoes a batch by reversing a batch from history, transforming that reversed batch and applying it. This is
  60. * a helper method for {@link undo.UndoCommand#_doExecute}.
  61. *
  62. * @private
  63. * @param {engine.model.Batch} batchToUndo A batch whose deltas will be reversed, transformed and applied.
  64. */
  65. _undo( batchToUndo ) {
  66. const document = this.editor.document;
  67. // All changes done by the command execution will be saved as one batch.
  68. const undoingBatch = document.batch();
  69. this._createdBatches.add( undoingBatch );
  70. const history = document.history;
  71. const deltasToUndo = batchToUndo.deltas.slice();
  72. deltasToUndo.reverse();
  73. // We will process each delta from `batchToUndo`, in reverse order. If there was deltas A, B and C in undone batch,
  74. // we need to revert them in reverse order, so first reverse C, then B, then A.
  75. for ( let deltaToUndo of deltasToUndo ) {
  76. // Keep in mind that all algorithms return arrays. That's because the transformation might result in multiple
  77. // deltas, so we need arrays to handle them anyway. To simplify algorithms, it is better to always have arrays
  78. // in mind. For simplicity reasons, we will use singular form in descriptions and names.
  79. const baseVersion = deltaToUndo.baseVersion;
  80. const nextBaseVersion = baseVersion + deltaToUndo.operations.length;
  81. // 1. Get updated version of the delta from the history.
  82. // Batch stored in the undo command might have an outdated version of the delta that should be undone.
  83. // To prevent errors, we will take an updated version of it from the history, basing on delta's `baseVersion`.
  84. const updatedDeltaToUndo = history.getDelta( baseVersion );
  85. // This is a safe valve in case of not finding delta to undo in history. This may come up if that delta
  86. // got updated into no deltas, or removed from history.
  87. if ( updatedDeltaToUndo === null ) {
  88. continue;
  89. }
  90. // 2. Reverse delta from the history.
  91. updatedDeltaToUndo.reverse();
  92. let reversedDelta = [];
  93. for ( let delta of updatedDeltaToUndo ) {
  94. reversedDelta.push( delta.getReversed() );
  95. }
  96. // Stores history deltas transformed by `deltaToUndo`. Will be used later for updating document history.
  97. const updatedHistoryDeltas = {};
  98. // 3. Transform reversed delta by history deltas that happened after delta to undo. We have to bring
  99. // reversed delta to the current state of document. While doing this, we will also update history deltas
  100. // to the state which "does not remember" delta that we undo.
  101. for ( let historyDelta of history.getDeltas( nextBaseVersion ) ) {
  102. // 3.1. Transform selection range stored with history batch by reversed delta.
  103. // It is important to keep stored selection ranges updated. As we are removing and updating deltas in the history,
  104. // selection ranges would base on outdated history state.
  105. const itemIndex = this._getItemIndexFromBaseVersion( historyDelta.baseVersion );
  106. // `itemIndex` will be `null` for `historyDelta` if it is not the first delta in it's batch.
  107. // This is fine, because we want to transform each selection only once, before transforming reversed delta
  108. // by the first delta of the batch connected with the ranges.
  109. if ( itemIndex !== null ) {
  110. this._stack[ itemIndex ].selection.ranges = transformRangesByDeltas( this._stack[ itemIndex ].selection.ranges, reversedDelta );
  111. }
  112. // 3.2. Transform history delta by reversed delta. We need this to update document history.
  113. const updatedHistoryDelta = transformDelta( [ historyDelta ], reversedDelta, false );
  114. // 3.3. Transform reversed delta by history delta (in state before transformation above).
  115. reversedDelta = transformDelta( reversedDelta, [ historyDelta ], true );
  116. // 3.4. Store updated history delta. Later, it will be updated in `history`.
  117. if ( !updatedHistoryDeltas[ historyDelta.baseVersion ] ) {
  118. updatedHistoryDeltas[ historyDelta.baseVersion ] = [];
  119. }
  120. updatedHistoryDeltas[ historyDelta.baseVersion ] = updatedHistoryDeltas[ historyDelta.baseVersion ].concat( updatedHistoryDelta );
  121. }
  122. // 4. After reversed delta has been transformed by all history deltas, apply it.
  123. for ( let delta of reversedDelta ) {
  124. // Fix base version.
  125. delta.baseVersion = document.version;
  126. // Before applying, add the delta to the `undoingBatch`.
  127. undoingBatch.addDelta( delta );
  128. // Now, apply all operations of the delta.
  129. for ( let operation of delta.operations ) {
  130. document.applyOperation( operation );
  131. }
  132. }
  133. // 5. Remove reversed delta from the history.
  134. history.removeDelta( baseVersion );
  135. // And all deltas that are reversing it.
  136. // So the history looks like both original and reversing deltas never happened.
  137. // That's why we have to update history deltas - some of them might have been basing on deltas that we are now removing.
  138. for ( let delta of reversedDelta ) {
  139. history.removeDelta( delta.baseVersion );
  140. }
  141. // 6. Update history deltas in history.
  142. for ( let historyBaseVersion in updatedHistoryDeltas ) {
  143. history.updateDelta( Number( historyBaseVersion ), updatedHistoryDeltas[ historyBaseVersion ] );
  144. }
  145. }
  146. return undoingBatch;
  147. }
  148. }