mergeoperation.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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.createFromPosition( 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.createFromPosition( 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.createFromPosition( 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. return new SplitOperation( this.targetPosition, this.howMany, this.graveyardPosition, this.baseVersion + 1 );
  109. }
  110. /**
  111. * @inheritDoc
  112. */
  113. _validate() {
  114. const sourceElement = this.sourcePosition.parent;
  115. const targetElement = this.targetPosition.parent;
  116. // Validate whether merge operation has correct parameters.
  117. if ( !sourceElement || !sourceElement.is( 'element' ) ) {
  118. /**
  119. * Merge source position is invalid.
  120. *
  121. * @error merge-operation-source-position-invalid
  122. */
  123. throw new CKEditorError( 'merge-operation-source-position-invalid: Merge source position is invalid.' );
  124. } else if ( !targetElement || !targetElement.is( 'element' ) ) {
  125. /**
  126. * Merge target position is invalid.
  127. *
  128. * @error merge-operation-target-position-invalid
  129. */
  130. throw new CKEditorError( 'merge-operation-target-position-invalid: Merge target position is invalid.' );
  131. } else if ( this.howMany != sourceElement.maxOffset ) {
  132. /**
  133. * Merge operation specifies wrong number of nodes to move.
  134. *
  135. * @error merge-operation-how-many-invalid
  136. */
  137. throw new CKEditorError( 'merge-operation-how-many-invalid: Merge operation specifies wrong number of nodes to move.' );
  138. }
  139. }
  140. /**
  141. * @inheritDoc
  142. */
  143. _execute() {
  144. const mergedElement = this.sourcePosition.parent;
  145. const sourceRange = Range.createIn( mergedElement );
  146. _move( sourceRange, this.targetPosition );
  147. _move( Range.createOn( mergedElement ), this.graveyardPosition );
  148. }
  149. /**
  150. * @inheritDoc
  151. */
  152. toJSON() {
  153. const json = super.toJSON();
  154. json.sourcePosition = json.sourcePosition.toJSON();
  155. json.targetPosition = json.targetPosition.toJSON();
  156. json.graveyardPosition = json.graveyardPosition.toJSON();
  157. return json;
  158. }
  159. /**
  160. * @inheritDoc
  161. */
  162. static get className() {
  163. return 'MergeOperation';
  164. }
  165. /**
  166. * Creates `MergeOperation` object from deserilized object, i.e. from parsed JSON string.
  167. *
  168. * @param {Object} json Deserialized JSON object.
  169. * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
  170. * @returns {module:engine/model/operation/mergeoperation~MergeOperation}
  171. */
  172. static fromJSON( json, document ) {
  173. const sourcePosition = Position.fromJSON( json.sourcePosition, document );
  174. const targetPosition = Position.fromJSON( json.targetPosition, document );
  175. const graveyardPosition = Position.fromJSON( json.graveyardPosition, document );
  176. return new this( sourcePosition, json.howMany, targetPosition, graveyardPosition, json.baseVersion );
  177. }
  178. }