8
0

basecommand.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module undo/basecommand
  7. */
  8. import Command from '@ckeditor/ckeditor5-core/src/command';
  9. import { transformSets } from '@ckeditor/ckeditor5-engine/src/model/operation/transform';
  10. /**
  11. * Base class for undo feature commands: {@link module:undo/undocommand~UndoCommand} and {@link module:undo/redocommand~RedoCommand}.
  12. *
  13. * @protected
  14. * @extends module:core/command~Command
  15. */
  16. export default class BaseCommand extends Command {
  17. constructor( editor ) {
  18. super( editor );
  19. /**
  20. * Stack of items stored by the command. These are pairs of:
  21. *
  22. * * {@link module:engine/model/batch~Batch batch} saved by the command,
  23. * * {@link module:engine/model/selection~Selection selection} state at the moment of saving the batch.
  24. *
  25. * @protected
  26. * @member {Array} #_stack
  27. */
  28. this._stack = [];
  29. /**
  30. * Stores all batches that were created by this command.
  31. *
  32. * @protected
  33. * @member {WeakSet.<module:engine/model/batch~Batch>} #_createdBatches
  34. */
  35. this._createdBatches = new WeakSet();
  36. // Refresh state, so the command is inactive right after initialization.
  37. this.refresh();
  38. this.listenTo( editor.data, 'set', () => this.clearStack() );
  39. }
  40. /**
  41. * @inheritDoc
  42. */
  43. refresh() {
  44. this.isEnabled = this._stack.length > 0;
  45. }
  46. /**
  47. * Stores a batch in the command, together with the selection state of the {@link module:engine/model/document~Document document}
  48. * created by the editor which this command is registered to.
  49. *
  50. * @param {module:engine/model/batch~Batch} batch The batch to add.
  51. */
  52. addBatch( batch ) {
  53. const docSelection = this.editor.model.document.selection;
  54. const selection = {
  55. ranges: docSelection.hasOwnRange ? Array.from( docSelection.getRanges() ) : [],
  56. isBackward: docSelection.isBackward
  57. };
  58. this._stack.push( { batch, selection } );
  59. this.refresh();
  60. }
  61. /**
  62. * Removes all items from the stack.
  63. */
  64. clearStack() {
  65. this._stack = [];
  66. this.refresh();
  67. }
  68. /**
  69. * Restores the {@link module:engine/model/document~Document#selection document selection} state after a batch was undone.
  70. *
  71. * @protected
  72. * @param {Array.<module:engine/model/range~Range>} ranges Ranges to be restored.
  73. * @param {Boolean} isBackward A flag describing whether the restored range was selected forward or backward.
  74. * @param {Array.<module:engine/model/operation/operation~Operation>} operations Operations which has been applied
  75. * since selection has been stored.
  76. */
  77. _restoreSelection( ranges, isBackward, operations ) {
  78. const model = this.editor.model;
  79. const document = model.document;
  80. // This will keep the transformed selection ranges.
  81. const selectionRanges = [];
  82. // Transform all ranges from the restored selection.
  83. const transformedRangeGroups = ranges.map( range => range.getTransformedByOperations( operations ) );
  84. const allRanges = transformedRangeGroups.flat();
  85. for ( const rangeGroup of transformedRangeGroups ) {
  86. // While transforming there could appear ranges that are contained by other ranges, we shall ignore them.
  87. const transformed = rangeGroup.filter( range => !isRangeContainedByAnyOtherRange( range, allRanges ) );
  88. // After the range got transformed, we have an array of ranges. Some of those
  89. // ranges may be "touching" -- they can be next to each other and could be merged.
  90. normalizeRanges( transformed );
  91. // For each `range` from `ranges`, we take only one transformed range.
  92. // This is because we want to prevent situation where single-range selection
  93. // got transformed to multi-range selection. We will take the first range that
  94. // is not in the graveyard.
  95. const newRange = transformed.find(
  96. range => range.root != document.graveyard
  97. );
  98. // `transformedRange` might be `undefined` if transformed range ended up in graveyard.
  99. if ( newRange ) {
  100. selectionRanges.push( newRange );
  101. }
  102. }
  103. // @if CK_DEBUG_ENGINE // console.log( `Restored selection by undo: ${ selectionRanges.join( ', ' ) }` );
  104. // `selectionRanges` may be empty if all ranges ended up in graveyard. If that is the case, do not restore selection.
  105. if ( selectionRanges.length ) {
  106. model.change( writer => {
  107. writer.setSelection( selectionRanges, { backward: isBackward } );
  108. } );
  109. }
  110. }
  111. /**
  112. * Undoes a batch by reversing that batch, transforming reversed batch and finally applying it.
  113. * This is a helper method for {@link #execute}.
  114. *
  115. * @protected
  116. * @param {module:engine/model/batch~Batch} batchToUndo The batch to be undone.
  117. * @param {module:engine/model/batch~Batch} undoingBatch The batch that will contain undoing changes.
  118. */
  119. _undo( batchToUndo, undoingBatch ) {
  120. const model = this.editor.model;
  121. const document = model.document;
  122. // All changes done by the command execution will be saved as one batch.
  123. this._createdBatches.add( undoingBatch );
  124. const operationsToUndo = batchToUndo.operations.slice().filter( operation => operation.isDocumentOperation );
  125. operationsToUndo.reverse();
  126. // We will process each operation from `batchToUndo`, in reverse order. If there were operations A, B and C in undone batch,
  127. // we need to revert them in reverse order, so first C' (reversed C), then B', then A'.
  128. for ( const operationToUndo of operationsToUndo ) {
  129. const nextBaseVersion = operationToUndo.baseVersion + 1;
  130. const historyOperations = Array.from( document.history.getOperations( nextBaseVersion ) );
  131. const transformedSets = transformSets(
  132. [ operationToUndo.getReversed() ],
  133. historyOperations,
  134. {
  135. useRelations: true,
  136. document: this.editor.model.document,
  137. padWithNoOps: false,
  138. forceWeakRemove: true
  139. }
  140. );
  141. const reversedOperations = transformedSets.operationsA;
  142. // After reversed operation has been transformed by all history operations, apply it.
  143. for ( const operation of reversedOperations ) {
  144. // Before applying, add the operation to the `undoingBatch`.
  145. undoingBatch.addOperation( operation );
  146. model.applyOperation( operation );
  147. document.history.setOperationAsUndone( operationToUndo, operation );
  148. }
  149. }
  150. }
  151. }
  152. // Normalizes list of ranges by joining intersecting or "touching" ranges.
  153. //
  154. // @param {Array.<module:engine/model/range~Range>} ranges
  155. //
  156. function normalizeRanges( ranges ) {
  157. ranges.sort( ( a, b ) => a.start.isBefore( b.start ) ? -1 : 1 );
  158. for ( let i = 1; i < ranges.length; i++ ) {
  159. const previousRange = ranges[ i - 1 ];
  160. const joinedRange = previousRange.getJoined( ranges[ i ], true );
  161. if ( joinedRange ) {
  162. // Replace the ranges on the list with the new joined range.
  163. i--;
  164. ranges.splice( i, 2, joinedRange );
  165. }
  166. }
  167. }
  168. function isRangeContainedByAnyOtherRange( range, ranges ) {
  169. return ranges.some( otherRange => otherRange !== range && otherRange.containsRange( range, true ) );
  170. }