8
0

undocommand.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import Command from '../command/command.js';
  7. /**
  8. * Undo command stores batches in itself and is able to and apply reverted versions of them on the document.
  9. *
  10. * @memberOf undo
  11. */
  12. export default class UndoCommand extends Command {
  13. constructor( editor ) {
  14. super( editor );
  15. /**
  16. * Items that are pairs of:
  17. *
  18. * * batches which are saved by the command and,
  19. * * model selection state at the moment of saving the batch.
  20. *
  21. * @private
  22. * @member {Array} undo.UndoCommand#_items
  23. */
  24. this._items = [];
  25. }
  26. /**
  27. * Stores a batch in the command. Stored batches can be then reverted.
  28. *
  29. * @param {engine.treeModel.Batch} batch Batch to add.
  30. */
  31. addBatch( batch ) {
  32. const selection = {
  33. ranges: Array.from( this.editor.document.selection.getRanges() ),
  34. isBackward: this.editor.document.selection.isBackward
  35. };
  36. this._items.push( { batch, selection } );
  37. this.refreshState();
  38. }
  39. /**
  40. * Removes all batches from the stack.
  41. */
  42. clearStack() {
  43. this._items = [];
  44. this.refreshState();
  45. }
  46. /**
  47. * @inheritDoc
  48. */
  49. _checkEnabled() {
  50. return this._items.length > 0;
  51. }
  52. /**
  53. * Executes the command: reverts a {@link engine.treeModel.Batch batch} added to the command's stack,
  54. * applies it on the document and removes the batch from the stack.
  55. *
  56. * @protected
  57. * @fires undo.undoCommand#event:revert
  58. * @param {engine.treeModel.Batch} [batch] If set, batch that should be undone. If not set, the last added batch will be undone.
  59. */
  60. _doExecute( batch ) {
  61. let batchIndex;
  62. // If batch is not given, set `batchIndex` to the last index in command stack.
  63. // If it is given, find it on the stack.
  64. if ( !batch ) {
  65. batchIndex = this._items.length - 1;
  66. } else {
  67. batchIndex = this._items.findIndex( item => item.batch == batch );
  68. }
  69. const undoItem = this._items.splice( batchIndex, 1 )[ 0 ];
  70. // Get the batch to undo.
  71. const undoBatch = undoItem.batch;
  72. const undoDeltas = undoBatch.deltas.slice();
  73. // Deltas have to be applied in reverse order, so if batch did A B C, it has to do reversed C, reversed B, reversed A.
  74. undoDeltas.reverse();
  75. // Reverse the deltas from the batch, transform them, apply them.
  76. for ( let undoDelta of undoDeltas ) {
  77. const undoDeltaReversed = undoDelta.getReversed();
  78. const updatedDeltas = this.editor.document.history.getTransformedDelta( undoDeltaReversed );
  79. for ( let delta of updatedDeltas ) {
  80. for ( let operation of delta.operations ) {
  81. this.editor.document.applyOperation( operation );
  82. }
  83. }
  84. }
  85. // Get the selection state stored with this batch.
  86. const selectionState = undoItem.selection;
  87. // Take all selection ranges that were stored with undone batch.
  88. const ranges = selectionState.ranges;
  89. // The ranges will be transformed by deltas from history that took place
  90. // after the selection got stored.
  91. const deltas = this.editor.document.history.getDeltas( undoBatch.deltas[ 0 ].baseVersion );
  92. // This will keep the transformed ranges.
  93. const transformedRanges = [];
  94. for ( let originalRange of ranges ) {
  95. // We create `transformed` array. At the beginning it will have only the original range.
  96. // During transformation the original range will change or even break into smaller ranges.
  97. // After the range is broken into two ranges, we have to transform both of those ranges separately.
  98. // For that reason, we keep all transformed ranges in one array and operate on it.
  99. let transformed = [ originalRange ];
  100. for ( let delta of deltas ) {
  101. for ( let operation of delta.operations ) {
  102. // We look through all operations from all deltas.
  103. for ( let t = 0; t < transformed.length; t++ ) {
  104. // We transform every range by every operation.
  105. // We keep current state of transformation in `transformed` array and update it.
  106. let result;
  107. switch ( operation.type ) {
  108. case 'insert':
  109. result = transformed[ t ].getTransformedByInsertion(
  110. operation.position,
  111. operation.nodeList.length,
  112. true
  113. );
  114. break;
  115. case 'move':
  116. case 'remove':
  117. case 'reinsert':
  118. result = transformed[ t ].getTransformedByMove(
  119. operation.sourcePosition,
  120. operation.targetPosition,
  121. operation.howMany,
  122. true
  123. );
  124. break;
  125. }
  126. // If we have a transformation result, we substitute it in `transformed` array with
  127. // the range that got transformed. Keep in mind that the result is an array
  128. // and may contain multiple ranges.
  129. if ( result ) {
  130. transformed.splice( t, 1, ...result );
  131. // Fix iterator.
  132. t = t + result.length - 1;
  133. }
  134. }
  135. }
  136. }
  137. // After `originalRange` got transformed, we have an array of ranges. Some of those
  138. // ranges may be "touching" -- they can be next to each other and could be merged.
  139. // Let's do this. First, we have to sort those ranges because they don't have to be
  140. // in an order.
  141. transformed.sort( ( a, b ) => a.start.isBefore( b.start ) ? -1 : 1 );
  142. // Then we check if two consecutive ranges are touching. We can do it pair by pair
  143. // in one dimensional loop because ranges are sorted.
  144. for ( let i = 1 ; i < transformed.length; i++ ) {
  145. let a = transformed[ i - 1 ];
  146. let b = transformed[ i ];
  147. if ( a.end.isTouching( b.start ) ) {
  148. a.end = b.end;
  149. transformed.splice( i, 1 );
  150. i--;
  151. }
  152. }
  153. // For each `originalRange` from `ranges`, we take only one transformed range.
  154. // This is because we want to prevent situation where single-range selection
  155. // got transformed to mulit-range selection. We will take the first range that
  156. // is not in the graveyard.
  157. const transformedRange = transformed.find(
  158. ( range ) => range.start.root != this.editor.document.graveyard
  159. );
  160. if ( transformedRange ) {
  161. transformedRanges.push( transformedRange );
  162. }
  163. }
  164. // `transformedRanges` may be empty if all ranges ended up in graveyard.
  165. // If that is the case, do not restore selection.
  166. if ( transformedRanges.length ) {
  167. this.editor.document.selection.setRanges( transformedRanges, selectionState.isBackward );
  168. }
  169. this.refreshState();
  170. this.fire( 'revert', undoBatch );
  171. }
  172. }
  173. /**
  174. * Fired after `UndoCommand` reverts a batch.
  175. *
  176. * @event undo.UndoCommand#revert
  177. * @param {engine.treeModel.Batch} undoBatch The batch instance that got reverted.
  178. */