mergeoperation.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/operation/mergeoperation
  7. */
  8. import Operation from './operation';
  9. import SplitOperation from './splitoperation';
  10. import Position from '../position';
  11. import Range from '../range';
  12. import { _move } from './utils';
  13. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  14. /**
  15. * Operation to merge two {@link module:engine/model/element~Element elements}.
  16. *
  17. * The merged element is the parent of {@link ~MergeOperation#sourcePosition} and it is merged into the parent of
  18. * {@link ~MergeOperation#targetPosition}. All nodes from the merged element are moved to {@link ~MergeOperation#targetPosition}.
  19. *
  20. * The merged element is moved to the graveyard at {@link ~MergeOperation#graveyardPosition}.
  21. *
  22. * @extends module:engine/model/operation/operation~Operation
  23. */
  24. export default class MergeOperation extends Operation {
  25. /**
  26. * Creates a merge operation.
  27. *
  28. * @param {module:engine/model/position~Position} sourcePosition Position inside the merged element. All nodes from that
  29. * element after that position will be moved to {@link ~#targetPosition}.
  30. * @param {Number} howMany Summary offset size of nodes which will be moved from the merged element to the new parent.
  31. * @param {module:engine/model/position~Position} targetPosition Position which the nodes from the merged elements will be moved to.
  32. * @param {module:engine/model/position~Position} graveyardPosition Position in graveyard to which the merged element will be moved.
  33. * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
  34. * can be applied or `null` if the operation operates on detached (non-document) tree.
  35. */
  36. constructor( sourcePosition, howMany, targetPosition, graveyardPosition, baseVersion ) {
  37. super( baseVersion );
  38. /**
  39. * Position inside the merged element. All nodes from that element after that position will be moved to {@link ~#targetPosition}.
  40. *
  41. * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#sourcePosition
  42. */
  43. this.sourcePosition = Position._createAt( sourcePosition );
  44. // This is, and should always remain, the first position in its parent.
  45. this.sourcePosition.stickiness = 'toPrevious';
  46. /**
  47. * Summary offset size of nodes which will be moved from the merged element to the new parent.
  48. *
  49. * @member {Number} module:engine/model/operation/mergeoperation~MergeOperation#howMany
  50. */
  51. this.howMany = howMany;
  52. /**
  53. * Position which the nodes from the merged elements will be moved to.
  54. *
  55. * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#targetPosition
  56. */
  57. this.targetPosition = Position._createAt( targetPosition );
  58. // Except of a rare scenario in `MergeOperation` x `MergeOperation` transformation,
  59. // this is, and should always remain, the last position in its parent.
  60. this.targetPosition.stickiness = 'toNext';
  61. /**
  62. * Position in graveyard to which the merged element will be moved.
  63. *
  64. * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#graveyardPosition
  65. */
  66. this.graveyardPosition = Position._createAt( graveyardPosition );
  67. }
  68. /**
  69. * @inheritDoc
  70. */
  71. get type() {
  72. return 'merge';
  73. }
  74. /**
  75. * Position before the merged element (which will be deleted).
  76. *
  77. * @readonly
  78. * @type {module:engine/model/position~Position}
  79. */
  80. get deletionPosition() {
  81. return new Position( this.sourcePosition.root, this.sourcePosition.path.slice( 0, -1 ) );
  82. }
  83. /**
  84. * Artificial range that contains all the nodes from the merged element that will be moved to {@link ~MergeOperation#sourcePosition}.
  85. * The range starts at {@link ~MergeOperation#sourcePosition} and ends in the same parent, at `POSITIVE_INFINITY` offset.
  86. *
  87. * @readonly
  88. * @type {module:engine/model/range~Range}
  89. */
  90. get movedRange() {
  91. const end = this.sourcePosition.getShiftedBy( Number.POSITIVE_INFINITY );
  92. return new Range( this.sourcePosition, end );
  93. }
  94. /**
  95. * Creates and returns an operation that has the same parameters as this operation.
  96. *
  97. * @returns {module:engine/model/operation/mergeoperation~MergeOperation} Clone of this operation.
  98. */
  99. clone() {
  100. return new this.constructor( this.sourcePosition, this.howMany, this.targetPosition, this.graveyardPosition, this.baseVersion );
  101. }
  102. /**
  103. * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
  104. *
  105. * @returns {module:engine/model/operation/splitoperation~SplitOperation}
  106. */
  107. getReversed() {
  108. // Positions in this method are transformed by this merge operation because the split operation bases on
  109. // the context after this merge operation happened (because split operation reverses it).
  110. // So we need to acknowledge that the merge operation happened and those positions changed a little.
  111. const targetPosition = this.targetPosition._getTransformedByMergeOperation( this );
  112. const path = this.sourcePosition.path.slice( 0, -1 );
  113. const insertionPosition = new Position( this.sourcePosition.root, path )._getTransformedByMergeOperation( this );
  114. const split = new SplitOperation( targetPosition, this.howMany, this.graveyardPosition, this.baseVersion + 1 );
  115. split.insertionPosition = insertionPosition;
  116. return split;
  117. }
  118. /**
  119. * @inheritDoc
  120. */
  121. _validate() {
  122. const sourceElement = this.sourcePosition.parent;
  123. const targetElement = this.targetPosition.parent;
  124. // Validate whether merge operation has correct parameters.
  125. if ( !sourceElement || !sourceElement.is( 'element' ) || !sourceElement.parent ) {
  126. /**
  127. * Merge source position is invalid.
  128. *
  129. * @error merge-operation-source-position-invalid
  130. */
  131. throw new CKEditorError( 'merge-operation-source-position-invalid: Merge source position is invalid.' );
  132. } else if ( !targetElement || !targetElement.is( 'element' ) || !targetElement.parent ) {
  133. /**
  134. * Merge target position is invalid.
  135. *
  136. * @error merge-operation-target-position-invalid
  137. */
  138. throw new CKEditorError( 'merge-operation-target-position-invalid: Merge target position is invalid.' );
  139. } else if ( this.howMany != sourceElement.maxOffset ) {
  140. /**
  141. * Merge operation specifies wrong number of nodes to move.
  142. *
  143. * @error merge-operation-how-many-invalid
  144. */
  145. throw new CKEditorError( 'merge-operation-how-many-invalid: Merge operation specifies wrong number of nodes to move.' );
  146. }
  147. }
  148. /**
  149. * @inheritDoc
  150. */
  151. _execute() {
  152. const mergedElement = this.sourcePosition.parent;
  153. const sourceRange = Range.createIn( mergedElement );
  154. _move( sourceRange, this.targetPosition );
  155. _move( Range.createOn( mergedElement ), this.graveyardPosition );
  156. }
  157. /**
  158. * @inheritDoc
  159. */
  160. toJSON() {
  161. const json = super.toJSON();
  162. json.sourcePosition = json.sourcePosition.toJSON();
  163. json.targetPosition = json.targetPosition.toJSON();
  164. json.graveyardPosition = json.graveyardPosition.toJSON();
  165. return json;
  166. }
  167. /**
  168. * @inheritDoc
  169. */
  170. static get className() {
  171. return 'MergeOperation';
  172. }
  173. /**
  174. * Creates `MergeOperation` object from deserilized object, i.e. from parsed JSON string.
  175. *
  176. * @param {Object} json Deserialized JSON object.
  177. * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
  178. * @returns {module:engine/model/operation/mergeoperation~MergeOperation}
  179. */
  180. static fromJSON( json, document ) {
  181. const sourcePosition = Position.fromJSON( json.sourcePosition, document );
  182. const targetPosition = Position.fromJSON( json.targetPosition, document );
  183. const graveyardPosition = Position.fromJSON( json.graveyardPosition, document );
  184. return new this( sourcePosition, json.howMany, targetPosition, graveyardPosition, json.baseVersion );
  185. }
  186. }