basecommand.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. /**
  2. * @license Copyright (c) 2003-2017, 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/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~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 command is inactive just after initialization.
  36. this.refreshState();
  37. }
  38. /**
  39. * Stores a batch in the command, together with the selection state of the {@link module:engine/model/document~Document document}
  40. * created by the editor which this command is registered to.
  41. *
  42. * @param {module:engine/model/batch~Batch} batch The batch to add.
  43. */
  44. addBatch( batch ) {
  45. const selection = {
  46. ranges: Array.from( this.editor.document.selection.getRanges() ),
  47. isBackward: this.editor.document.selection.isBackward
  48. };
  49. this._stack.push( { batch, selection } );
  50. this.refreshState();
  51. }
  52. /**
  53. * Removes all items from the stack.
  54. */
  55. clearStack() {
  56. this._stack = [];
  57. this.refreshState();
  58. }
  59. /**
  60. * @inheritDoc
  61. */
  62. _checkEnabled() {
  63. return this._stack.length > 0;
  64. }
  65. /**
  66. * Restores the {@link module:engine/model/document~Document#selection document selection} state after a batch was undone.
  67. *
  68. * @protected
  69. * @param {Array.<module:engine/model/range~Range>} ranges Ranges to be restored.
  70. * @param {Boolean} isBackward A flag describing whether the restored range was selected forward or backward.
  71. */
  72. _restoreSelection( ranges, isBackward, deltas ) {
  73. const document = this.editor.document;
  74. // This will keep the transformed selection ranges.
  75. const selectionRanges = [];
  76. // Transform all ranges from the restored selection.
  77. for ( let range of ranges ) {
  78. const transformedRanges = transformSelectionRange( range, deltas );
  79. // For each `range` from `ranges`, we take only one transformed range.
  80. // This is because we want to prevent situation where single-range selection
  81. // got transformed to multi-range selection. We will take the first range that
  82. // is not in the graveyard.
  83. const transformedRange = transformedRanges.find(
  84. ( range ) => range.start.root != document.graveyard
  85. );
  86. // `transformedRange` might be `undefined` if transformed range ended up in graveyard.
  87. if ( transformedRange ) {
  88. selectionRanges.push( transformedRange );
  89. }
  90. }
  91. // `selectionRanges` may be empty if all ranges ended up in graveyard. If that is the case, do not restore selection.
  92. if ( selectionRanges.length ) {
  93. document.selection.setRanges( selectionRanges, isBackward );
  94. }
  95. }
  96. }
  97. // Transforms given range `range` by deltas from `document` history, starting from a delta with given `baseVersion`.
  98. // Returns an array containing one or more ranges, which are result of the transformation.
  99. function transformSelectionRange( range, deltas ) {
  100. // The range will be transformed by history deltas that happened after the selection got stored.
  101. // Note, that at this point, the document history is already updated by undo command execution. We will
  102. // not transform the range by deltas that got undone or their reversing counterparts.
  103. let transformed = transformRangesByDeltas( [ range ], deltas );
  104. // After `range` got transformed, we have an array of ranges. Some of those
  105. // ranges may be "touching" -- they can be next to each other and could be merged.
  106. // First, we have to sort those ranges because they don't have to be in an order.
  107. transformed.sort( ( a, b ) => a.start.isBefore( b.start ) ? -1 : 1 );
  108. // Then, we check if two consecutive ranges are touching.
  109. for ( let i = 1 ; i < transformed.length; i++ ) {
  110. let a = transformed[ i - 1 ];
  111. let b = transformed[ i ];
  112. if ( a.end.isTouching( b.start ) ) {
  113. a.end = b.end;
  114. transformed.splice( i, 1 );
  115. i--;
  116. }
  117. }
  118. return transformed;
  119. }
  120. // Transforms given set of `ranges` by given set of `deltas`. Returns transformed `ranges`.
  121. export function transformRangesByDeltas( ranges, deltas ) {
  122. for ( let delta of deltas ) {
  123. for ( let operation of delta.operations ) {
  124. // We look through all operations from all deltas.
  125. for ( let i = 0; i < ranges.length; i++ ) {
  126. // We transform every range by every operation.
  127. let result;
  128. switch ( operation.type ) {
  129. case 'insert':
  130. result = ranges[ i ]._getTransformedByInsertion(
  131. operation.position,
  132. operation.nodes.maxOffset,
  133. true
  134. );
  135. break;
  136. case 'move':
  137. case 'remove':
  138. case 'reinsert':
  139. result = ranges[ i ]._getTransformedByMove(
  140. operation.sourcePosition,
  141. operation.targetPosition,
  142. operation.howMany,
  143. true
  144. );
  145. break;
  146. }
  147. // If we have a transformation result, we substitute transformed range with it in `transformed` array.
  148. // Keep in mind that the result is an array and may contain multiple ranges.
  149. if ( result ) {
  150. ranges.splice( i, 1, ...result );
  151. // Fix iterator.
  152. i = i + result.length - 1;
  153. }
  154. }
  155. }
  156. }
  157. return ranges;
  158. }