basecommand.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module undo/basecommand
  7. */
  8. import Command from '@ckeditor/ckeditor5-core/src/command';
  9. /**
  10. * Base class for undo feature commands: {@link module:undo/undocommand~UndoCommand} and {@link module:undo/redocommand~RedoCommand}.
  11. *
  12. * @protected
  13. * @extends module:core/command~Command
  14. */
  15. export default class BaseCommand extends Command {
  16. constructor( editor ) {
  17. super( editor );
  18. /**
  19. * Stack of items stored by the command. These are pairs of:
  20. *
  21. * * {@link module:engine/model/batch~Batch batch} saved by the command,
  22. * * {@link module:engine/model/selection~Selection selection} state at the moment of saving the batch.
  23. *
  24. * @protected
  25. * @member {Array} #_stack
  26. */
  27. this._stack = [];
  28. /**
  29. * Stores all batches that were created by this command.
  30. *
  31. * @protected
  32. * @member {WeakSet.<module:engine/model/batch~Batch>} #_createdBatches
  33. */
  34. this._createdBatches = new WeakSet();
  35. // Refresh state, so the command is inactive right after initialization.
  36. this.refresh();
  37. }
  38. /**
  39. * @inheritDoc
  40. */
  41. refresh() {
  42. this.isEnabled = this._stack.length > 0;
  43. }
  44. /**
  45. * Stores a batch in the command, together with the selection state of the {@link module:engine/model/document~Document document}
  46. * created by the editor which this command is registered to.
  47. *
  48. * @param {module:engine/model/batch~Batch} batch The batch to add.
  49. */
  50. addBatch( batch ) {
  51. const docSelection = this.editor.model.document.selection;
  52. const selection = {
  53. ranges: docSelection.hasOwnRange ? Array.from( docSelection.getRanges() ) : [],
  54. isBackward: docSelection.isBackward
  55. };
  56. this._stack.push( { batch, selection } );
  57. this.refresh();
  58. }
  59. /**
  60. * Removes all items from the stack.
  61. */
  62. clearStack() {
  63. this._stack = [];
  64. this.refresh();
  65. }
  66. /**
  67. * Restores the {@link module:engine/model/document~Document#selection document selection} state after a batch was undone.
  68. *
  69. * @protected
  70. * @param {Array.<module:engine/model/range~Range>} ranges Ranges to be restored.
  71. * @param {Boolean} isBackward A flag describing whether the restored range was selected forward or backward.
  72. * @param {Array.<module:engine/model/delta/delta~Delta>} deltas Deltas which has been applied since selection has been stored.
  73. */
  74. _restoreSelection( ranges, isBackward, deltas ) {
  75. const model = this.editor.model;
  76. const document = model.document;
  77. // This will keep the transformed selection ranges.
  78. const selectionRanges = [];
  79. // Transform all ranges from the restored selection.
  80. for ( const range of ranges ) {
  81. const transformedRanges = transformSelectionRange( range, deltas );
  82. // For each `range` from `ranges`, we take only one transformed range.
  83. // This is because we want to prevent situation where single-range selection
  84. // got transformed to multi-range selection. We will take the first range that
  85. // is not in the graveyard.
  86. const transformedRange = transformedRanges.find(
  87. range => range.start.root != document.graveyard
  88. );
  89. // `transformedRange` might be `undefined` if transformed range ended up in graveyard.
  90. if ( transformedRange ) {
  91. selectionRanges.push( transformedRange );
  92. }
  93. }
  94. // `selectionRanges` may be empty if all ranges ended up in graveyard. If that is the case, do not restore selection.
  95. if ( selectionRanges.length ) {
  96. model.change( writer => {
  97. writer.setSelection( selectionRanges, { backward: isBackward } );
  98. } );
  99. }
  100. }
  101. /**
  102. * Undoes a batch by reversing that batch, transforming reversed batch and finally applying it.
  103. * This is a helper method for {@link #execute}.
  104. *
  105. * @protected
  106. * @param {module:engine/model/batch~Batch} batchToUndo The batch to be undone.
  107. * @param {module:engine/model/batch~Batch} undoingBatch The batch that will contain undoing changes.
  108. */
  109. _undo( batchToUndo, undoingBatch ) {
  110. const model = this.editor.model;
  111. const document = model.document;
  112. // All changes done by the command execution will be saved as one batch.
  113. this._createdBatches.add( undoingBatch );
  114. const deltasToUndo = batchToUndo.deltas.slice();
  115. deltasToUndo.reverse();
  116. // We will process each delta from `batchToUndo`, in reverse order. If there were deltas A, B and C in undone batch,
  117. // we need to revert them in reverse order, so first C' (reversed C), then B', then A'.
  118. for ( const deltaToUndo of deltasToUndo ) {
  119. // For now let's skip deltas with operation applied on detached document.
  120. // We assumed that there is no deltas with mixed (document and document fragment) operations
  121. // so we can skip entire delta.
  122. if ( deltaToUndo.operations.some( op => op.isDocumentOperation ) ) {
  123. // Keep in mind that transformation algorithms return arrays. That's because the transformation might result in multiple
  124. // deltas, so we need arrays to handle them. To simplify algorithms, it is better to always operate on arrays.
  125. const nextBaseVersion = deltaToUndo.baseVersion + deltaToUndo.operations.length;
  126. // Reverse delta from the history.
  127. const historyDeltas = Array.from( document.history.getDeltas( nextBaseVersion ) );
  128. const transformedSets = model.transformDeltas( [ deltaToUndo.getReversed() ], historyDeltas, true );
  129. const reversedDeltas = transformedSets.deltasA;
  130. // After reversed delta has been transformed by all history deltas, apply it.
  131. for ( const delta of reversedDeltas ) {
  132. // Fix base version.
  133. delta.baseVersion = document.version;
  134. // Before applying, add the delta to the `undoingBatch`.
  135. undoingBatch.addDelta( delta );
  136. // Now, apply all operations of the delta.
  137. for ( const operation of delta.operations ) {
  138. model.applyOperation( operation );
  139. }
  140. document.history.setDeltaAsUndone( deltaToUndo, delta );
  141. }
  142. }
  143. }
  144. }
  145. }
  146. // Transforms given range `range` by given `deltas`.
  147. // Returns an array containing one or more ranges, which are result of the transformation.
  148. function transformSelectionRange( range, deltas ) {
  149. const transformed = transformRangesByDeltas( [ range ], deltas );
  150. // After `range` got transformed, we have an array of ranges. Some of those
  151. // ranges may be "touching" -- they can be next to each other and could be merged.
  152. // First, we have to sort those ranges to assure that they are in order.
  153. transformed.sort( ( a, b ) => a.start.isBefore( b.start ) ? -1 : 1 );
  154. // Then, we check if two consecutive ranges are touching.
  155. for ( let i = 1; i < transformed.length; i++ ) {
  156. const a = transformed[ i - 1 ];
  157. const b = transformed[ i ];
  158. if ( a.end.isTouching( b.start ) ) {
  159. // And join them together if they are.
  160. a.end = b.end;
  161. transformed.splice( i, 1 );
  162. i--;
  163. }
  164. }
  165. return transformed;
  166. }
  167. // Transforms given set of `ranges` by given set of `deltas`. Returns transformed `ranges`.
  168. export function transformRangesByDeltas( ranges, deltas ) {
  169. for ( const delta of deltas ) {
  170. for ( const operation of delta.operations ) {
  171. // We look through all operations from all deltas.
  172. for ( let i = 0; i < ranges.length; i++ ) {
  173. // We transform every range by every operation.
  174. let result;
  175. switch ( operation.type ) {
  176. case 'insert':
  177. result = ranges[ i ]._getTransformedByInsertion(
  178. operation.position,
  179. operation.nodes.maxOffset,
  180. true
  181. );
  182. break;
  183. case 'move':
  184. case 'remove':
  185. case 'reinsert':
  186. result = ranges[ i ]._getTransformedByMove(
  187. operation.sourcePosition,
  188. operation.targetPosition,
  189. operation.howMany,
  190. true
  191. );
  192. break;
  193. }
  194. // If we have a transformation result, we substitute transformed range with it in `transformed` array.
  195. // Keep in mind that the result is an array and may contain multiple ranges.
  196. if ( result ) {
  197. ranges.splice( i, 1, ...result );
  198. // Fix iterator.
  199. i = i + result.length - 1;
  200. }
  201. }
  202. }
  203. }
  204. return ranges;
  205. }