moveoperation.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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 Operation from './operation.js';
  7. import Position from '../position.js';
  8. import Range from '../range.js';
  9. import CKEditorError from '../../../utils/ckeditorerror.js';
  10. import compareArrays from '../../../utils/comparearrays.js';
  11. /**
  12. * Operation to move list of subsequent nodes from one position in the document to another.
  13. *
  14. * @memberOf engine.treeModel.operation
  15. * @extends engine.treeModel.operation.Operation
  16. */
  17. export default class MoveOperation extends Operation {
  18. /**
  19. * Creates a move operation.
  20. *
  21. * @param {engine.treeModel.Position} sourcePosition Position before the first node to move.
  22. * @param {Number} howMany How many consecutive nodes to move, starting from `sourcePosition`.
  23. * @param {engine.treeModel.Position} targetPosition Position where moved nodes will be inserted.
  24. * @param {Number} baseVersion {@link engine.treeModel.Document#version} on which operation can be applied.
  25. */
  26. constructor( sourcePosition, howMany, targetPosition, baseVersion ) {
  27. super( baseVersion );
  28. /**
  29. * Source move position.
  30. *
  31. * @member {engine.treeModel.Position} engine.treeModel.operation.MoveOperation#sourcePosition
  32. */
  33. this.sourcePosition = Position.createFromPosition( sourcePosition );
  34. /**
  35. * How many nodes to move.
  36. *
  37. * @member {Number} engine.treeModel.operation.MoveOperation#howMany
  38. */
  39. this.howMany = howMany;
  40. /**
  41. * Target move position.
  42. *
  43. * @member {engine.treeModel.Position} engine.treeModel.operation.MoveOperation#targetPosition
  44. */
  45. this.targetPosition = Position.createFromPosition( targetPosition );
  46. /**
  47. * Position of the start of the moved range after it got moved. This may be different than
  48. * {@link engine.treeModel.operation.MoveOperation#targetPosition} in some cases, i.e. when a range is moved
  49. * inside the same parent but {@link engine.treeModel.operation.MoveOperation#targetPosition targetPosition}
  50. * is after {@link engine.treeModel.operation.MoveOperation#sourcePosition sourcePosition}.
  51. *
  52. * vv vv
  53. * abcdefg ===> adefbcg
  54. * ^ ^
  55. * targetPos movedRangeStart
  56. * offset 6 offset 4
  57. *
  58. * @member {engine.treeModel.Position} engine.treeModel.operation.MoveOperation#movedRangeStart
  59. */
  60. this.movedRangeStart = this.targetPosition.getTransformedByDeletion( this.sourcePosition, this.howMany );
  61. /**
  62. * Defines whether `MoveOperation` is sticky. If `MoveOperation` is sticky, during
  63. * {@link engine.treeModel.operation.transform operational transformation} if there will be an operation that
  64. * inserts some nodes at the position equal to the boundary of this `MoveOperation`, that operation will
  65. * get their insertion path updated to the position where this `MoveOperation` moves the range.
  66. *
  67. * @member {Boolean} engine.treeModel.operation.MoveOperation#isSticky
  68. */
  69. this.isSticky = false;
  70. }
  71. get type() {
  72. return 'move';
  73. }
  74. /**
  75. * @returns {engine.treeModel.operation.MoveOperation}
  76. */
  77. clone() {
  78. const op = new this.constructor( this.sourcePosition, this.howMany, this.targetPosition, this.baseVersion );
  79. op.isSticky = this.isSticky;
  80. return op;
  81. }
  82. /**
  83. * @returns {engine.treeModel.operation.MoveOperation}
  84. */
  85. getReversed() {
  86. let newTargetPosition = this.sourcePosition.getTransformedByInsertion( this.targetPosition, this.howMany );
  87. const op = new this.constructor( this.movedRangeStart, this.howMany, newTargetPosition, this.baseVersion + 1 );
  88. op.isSticky = this.isSticky;
  89. return op;
  90. }
  91. _execute() {
  92. let sourceElement = this.sourcePosition.parent;
  93. let targetElement = this.targetPosition.parent;
  94. let sourceOffset = this.sourcePosition.offset;
  95. let targetOffset = this.targetPosition.offset;
  96. // Validate whether move operation has correct parameters.
  97. // Validation is pretty complex but move operation is one of the core ways to manipulate the document state.
  98. // We expect that many errors might be connected with one of scenarios described below.
  99. if ( !sourceElement || !targetElement ) {
  100. /**
  101. * Source position or target position is invalid.
  102. *
  103. * @error operation-move-position-invalid
  104. */
  105. throw new CKEditorError(
  106. 'operation-move-position-invalid: Source position or target position is invalid.'
  107. );
  108. } else if ( sourceOffset + this.howMany > sourceElement.getChildCount() ) {
  109. /**
  110. * The nodes which should be moved do not exist.
  111. *
  112. * @error operation-move-nodes-do-not-exist
  113. */
  114. throw new CKEditorError(
  115. 'operation-move-nodes-do-not-exist: The nodes which should be moved do not exist.'
  116. );
  117. } else if ( sourceElement === targetElement && sourceOffset < targetOffset && targetOffset < sourceOffset + this.howMany ) {
  118. /**
  119. * Trying to move a range of nodes into the middle of that range.
  120. *
  121. * @error operation-move-range-into-itself
  122. */
  123. throw new CKEditorError(
  124. 'operation-move-range-into-itself: Trying to move a range of nodes to the inside of that range.'
  125. );
  126. } else {
  127. if ( compareArrays( this.sourcePosition.getParentPath(), this.targetPosition.getParentPath() ) == 'PREFIX' ) {
  128. let i = this.sourcePosition.path.length - 1;
  129. if ( this.targetPosition.path[ i ] >= sourceOffset && this.targetPosition.path[ i ] < sourceOffset + this.howMany ) {
  130. /**
  131. * Trying to move a range of nodes into one of nodes from that range.
  132. *
  133. * @error operation-move-node-into-itself
  134. */
  135. throw new CKEditorError(
  136. 'operation-move-node-into-itself: Trying to move a range of nodes into one of nodes from that range.'
  137. );
  138. }
  139. }
  140. }
  141. // End of validation.
  142. // If we move children in the same element and we remove elements on the position before the target we
  143. // need to update a target offset.
  144. if ( sourceElement === targetElement && sourceOffset < targetOffset ) {
  145. targetOffset -= this.howMany;
  146. }
  147. const removedNodes = sourceElement.removeChildren( sourceOffset, this.howMany );
  148. targetElement.insertChildren( targetOffset, removedNodes );
  149. return {
  150. sourcePosition: this.sourcePosition,
  151. range: Range.createFromPositionAndShift( this.movedRangeStart, this.howMany )
  152. };
  153. }
  154. /**
  155. * @inheritDoc
  156. */
  157. static get className() {
  158. return 'engine.treeModel.operation.MoveOperation';
  159. }
  160. /**
  161. * @inheritDoc
  162. */
  163. static fromJSON( json, doc ) {
  164. let sourcePosition = Position.fromJSON( json.sourcePosition, doc );
  165. let targetPosition = Position.fromJSON( json.targetPosition, doc );
  166. return new MoveOperation( sourcePosition, json.howMany, targetPosition, json.baseVersion );
  167. }
  168. }