mergeoperation.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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}. The merged elements are a parent of given
  16. * {@link module:engine/model/position~Position position} and the next element.
  17. *
  18. * Technically, the content of the next element is moved at given {@link module:engine/model/position~Position position}
  19. * and the element is removed.
  20. *
  21. * @extends module:engine/model/operation/operation~Operation
  22. */
  23. export default class MergeOperation extends Operation {
  24. /**
  25. * Creates a merge operation.
  26. *
  27. * @param {module:engine/model/position~Position} sourcePosition Position inside the merged element. All nodes from that
  28. * element after that position will be moved to {@link ~#targetPosition}.
  29. * @param {Number} howMany Summary offset size of nodes which will be moved from the merged element to the new parent.
  30. * @param {module:engine/model/position~Position} targetPosition Position which the nodes from the merged elements will be moved to.
  31. * @param {module:engine/model/position~Position} graveyardPosition Position in graveyard to which the merged element will be moved.
  32. * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
  33. * can be applied or `null` if the operation operates on detached (non-document) tree.
  34. */
  35. constructor( sourcePosition, howMany, targetPosition, graveyardPosition, baseVersion ) {
  36. super( baseVersion );
  37. /**
  38. * Position inside the merged element. All nodes from that element after that position will be moved to {@link ~#targetPosition}.
  39. *
  40. * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#sourcePosition
  41. */
  42. this.sourcePosition = Position.createFromPosition( sourcePosition );
  43. this.sourcePosition.stickiness = 'toPrevious'; // This is, and should always remain, the first position in its parent.
  44. /**
  45. * Position which the nodes from the merged elements will be moved to.
  46. *
  47. * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#targetPosition
  48. */
  49. this.targetPosition = Position.createFromPosition( targetPosition );
  50. this.targetPosition.stickiness = 'toNext'; // This is, and should always remain, the last position in its parent.
  51. // is it? think about reversed split operations, undo, etc.
  52. this.graveyardPosition = Position.createFromPosition( graveyardPosition );
  53. this.howMany = howMany;
  54. }
  55. /**
  56. * @inheritDoc
  57. */
  58. get type() {
  59. return 'merge';
  60. }
  61. /**
  62. * Position before the merged element (which will be removed). Calculated based on the split position.
  63. *
  64. * @readonly
  65. * @type {module:engine/model/position~Position}
  66. */
  67. get deletionPosition() {
  68. return new Position( this.sourcePosition.root, this.sourcePosition.path.slice( 0, -1 ) );
  69. }
  70. /**
  71. * Artificial range that contains all the nodes from the merged element that will be moved to {@link ~#sourcePosition}.
  72. * The range starts at {@link ~#sourcePosition} and ends in the same parent, at `POSITIVE_INFINITY` offset.
  73. *
  74. * @readonly
  75. * @type {module:engine/model/range~Range}
  76. */
  77. get movedRange() {
  78. const end = this.sourcePosition.getShiftedBy( Number.POSITIVE_INFINITY );
  79. return new Range( this.sourcePosition, end );
  80. }
  81. /**
  82. * Creates and returns an operation that has the same parameters as this operation.
  83. *
  84. * @returns {module:engine/model/operation/mergeoperation~MergeOperation} Clone of this operation.
  85. */
  86. clone() {
  87. return new this.constructor( this.sourcePosition, this.howMany, this.targetPosition, this.graveyardPosition, this.baseVersion );
  88. }
  89. /**
  90. * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
  91. *
  92. * @returns {module:engine/model/operation/splitoperation~SplitOperation}
  93. */
  94. getReversed() {
  95. return new SplitOperation( this.targetPosition, this.howMany, this.graveyardPosition, this.baseVersion + 1 );
  96. }
  97. /**
  98. * @inheritDoc
  99. */
  100. _validate() {
  101. const sourceElement = this.sourcePosition.parent;
  102. const targetElement = this.targetPosition.parent;
  103. // Validate whether merge operation has correct parameters.
  104. if ( !sourceElement || !sourceElement.is( 'element' ) ) {
  105. /**
  106. * Merge source position is invalid.
  107. *
  108. * @error merge-operation-source-position-invalid
  109. */
  110. throw new CKEditorError( 'merge-operation-source-position-invalid: Merge source position is invalid.' );
  111. } else if ( !targetElement || !targetElement.is( 'element' ) ) {
  112. /**
  113. * Merge target position is invalid.
  114. *
  115. * @error merge-operation-target-position-invalid
  116. */
  117. throw new CKEditorError( 'merge-operation-target-position-invalid: Merge target position is invalid.' );
  118. } else if ( this.howMany != sourceElement.maxOffset ) {
  119. /**
  120. * Merge operation specifies wrong number of nodes to move.
  121. *
  122. * @error merge-operation-how-many-invalid
  123. */
  124. throw new CKEditorError( 'merge-operation-how-many-invalid: Merge operation specifies wrong number of nodes to move.' );
  125. }
  126. }
  127. /**
  128. * @inheritDoc
  129. */
  130. _execute() {
  131. const mergedElement = this.sourcePosition.parent;
  132. const sourceRange = Range.createIn( mergedElement );
  133. _move( sourceRange, this.targetPosition );
  134. _move( Range.createOn( mergedElement ), this.graveyardPosition );
  135. }
  136. /**
  137. * @inheritDoc
  138. */
  139. toJSON() {
  140. const json = super.toJSON();
  141. json.sourcePosition = json.sourcePosition.toJSON();
  142. json.targetPosition = json.targetPosition.toJSON();
  143. json.graveyardPosition = json.graveyardPosition.toJSON();
  144. return json;
  145. }
  146. /**
  147. * @inheritDoc
  148. */
  149. static get className() {
  150. return 'MergeOperation';
  151. }
  152. /**
  153. * Creates `MergeOperation` object from deserilized object, i.e. from parsed JSON string.
  154. *
  155. * @param {Object} json Deserialized JSON object.
  156. * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
  157. * @returns {module:engine/model/operation/mergeoperation~MergeOperation}
  158. */
  159. static fromJSON( json, document ) {
  160. const sourcePosition = Position.fromJSON( json.sourcePosition, document );
  161. const targetPosition = Position.fromJSON( json.targetPosition, document );
  162. const graveyardPosition = Position.fromJSON( json.graveyardPosition, document );
  163. return new this( sourcePosition, json.howMany, targetPosition, graveyardPosition, json.baseVersion );
  164. }
  165. }