range.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. CKEDITOR.define( [ 'document/position', 'document/positioniterator', 'utils' ], ( Position, PositionIterator, utils ) => {
  7. /**
  8. * Range class. Range is iterable.
  9. *
  10. * @class document.Range
  11. */
  12. class Range {
  13. /**
  14. * Creates a range.
  15. *
  16. * @param {document.Position} start Start position.
  17. * @param {document.Position} end End position.
  18. * @constructor
  19. */
  20. constructor( start, end ) {
  21. /**
  22. * Start position.
  23. *
  24. * @property {document.Position}
  25. */
  26. this.start = start;
  27. /**
  28. * End position.
  29. *
  30. * @property {document.Position}
  31. */
  32. this.end = end;
  33. }
  34. /**
  35. * Creates a range inside an element which starts before the first child and ends after the last child.
  36. *
  37. * @param {document.Element} element Element which is a parent for the range.
  38. * @returns {document.Range} Created range.
  39. */
  40. static createFromElement( element ) {
  41. return Range.createFromParentsAndOffsets( element, 0, element, element.getChildCount() );
  42. }
  43. /**
  44. * Creates a range from given parents and offsets.
  45. *
  46. * @param {document.Element} startElement Start position parent element.
  47. * @param {Number} startOffset Start position offset.
  48. * @param {document.Element} endElement End position parent element.
  49. * @param {Number} endOffset End position offset.
  50. * @returns {document.Range} Created range.
  51. */
  52. static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
  53. return new Range(
  54. Position.createFromParentAndOffset( startElement, startOffset ),
  55. Position.createFromParentAndOffset( endElement, endOffset )
  56. );
  57. }
  58. /**
  59. * Two ranges equal if their start and end positions equal.
  60. *
  61. * @param {document.Range} otherRange Range to compare with.
  62. * @returns {Boolean} True if ranges equal.
  63. */
  64. isEqual( otherRange ) {
  65. return this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end );
  66. }
  67. /**
  68. * Creates and returns a new instance of {@link document.Range}
  69. * that is equal to this {@link document.Range range}.
  70. *
  71. * @returns {document.Position} Cloned {@link document.Range range}.
  72. */
  73. clone() {
  74. return new Range( this.start.clone(), this.end.clone() );
  75. }
  76. /**
  77. * Checks whether this {document.Range range} contains given (document.Position position}.
  78. *
  79. * @param {document.Position} position Position to check.
  80. * @returns {boolean} True if given {document.Position position} is contained.
  81. */
  82. containsPosition( position ) {
  83. return position.isAfter( this.start ) && position.isBefore( this.end );
  84. }
  85. /**
  86. * Returns an array containing one or two {document.Range ranges} that are results of transforming this
  87. * {document.Range range} by inserting `howMany` nodes at `insertPosition`. Two {document.Range ranges} are
  88. * returned if the insertion was inside this {document.Range range}.
  89. *
  90. * Examples:
  91. * let range = new Range( new Position( [ 2, 7 ], root ), new Position( [ 4, 0, 1 ], root ) );
  92. * let transformed = range.getTransformedByInsertion( new Position( [ 1 ], root ), 2 );
  93. * // transformed array has one range from [ 4, 7 ] to [ 6, 0, 1 ]
  94. *
  95. * transformed = range.getTransformedByInsertion( new Position( [ 3, 2 ], root ), 4 );
  96. * // transformed array has two ranges: from [ 2, 7 ] to [ 3, 2 ] and from [ 3, 6 ] to [ 4, 0, 1 ]
  97. *
  98. * transformed = range.getTransformedByInsertion( new Position( [ 3, 2 ], root ), 4, true );
  99. * // transformed array has one range which is equal to `range`. This is because of spreadOnlyOnSameLevel flag.
  100. *
  101. * @param {document.Position} insertPosition Position where nodes are inserted.
  102. * @param {Number} howMany How many nodes are inserted.
  103. * @param {Boolean} spreadOnlyOnSameLevel Flag indicating whether this {document.Range range} should be spread
  104. * if insertion was inside a node from this {document.Range range} but not in the range itself.
  105. * @returns {Array.<document.Range>} Result of the transformation.
  106. */
  107. getTransformedByInsertion( insertPosition, howMany, spreadOnlyOnSameLevel ) {
  108. // Flag indicating whether this whole range and given insertPosition is on the same tree level.
  109. const areOnSameLevel = utils.compareArrays( this.start.getParentPath(), this.end.getParentPath() ) == utils.compareArrays.SAME &&
  110. utils.compareArrays( this.start.getParentPath(), insertPosition.getParentPath() ) == utils.compareArrays.SAME;
  111. if ( this.containsPosition( insertPosition ) && ( !spreadOnlyOnSameLevel || areOnSameLevel ) ) {
  112. // Range has to be spread. The first part is from original start to the spread point.
  113. // The other part is from spread point to the original end, but transformed by
  114. // insertion to reflect insertion changes.
  115. return [
  116. new Range(
  117. insertPosition.getTransformedByInsertion( insertPosition, howMany, true ),
  118. this.end.getTransformedByInsertion( insertPosition, howMany, true )
  119. ),
  120. new Range(
  121. this.start.clone(),
  122. insertPosition.clone()
  123. )
  124. ];
  125. } else {
  126. // If insertion is not inside the range, simply transform range boundaries (positions) by the insertion.
  127. // Both, one or none of them might be affected by the insertion.
  128. const range = this.clone();
  129. range.start = range.start.getTransformedByInsertion( insertPosition, howMany, true );
  130. range.end = range.end.getTransformedByInsertion( insertPosition, howMany, false );
  131. return [ range ];
  132. }
  133. }
  134. /**
  135. * Gets a part of this {document.Range range} that is not a part of given {document.Range range}. Returned
  136. * array contains zero, one or two {document.Range ranges}.
  137. *
  138. * Examples:
  139. * let range = new Range( new Position( [ 2, 7 ], root ), new Position( [ 4, 0, 1 ], root ) );
  140. * let otherRange = new Range( new Position( [ 1 ], root ), new Position( [ 5 ], root ) );
  141. * let transformed = range.getDifference( otherRange );
  142. * // transformed array has no ranges because `otherRange` contains `range`
  143. *
  144. * otherRange = new Range( new Position( [ 1 ], root ), new Position( [ 3 ], root ) );
  145. * transformed = range.getDifference( otherRange );
  146. * // transformed array has one range: from [ 3 ] to [ 4, 0, 1 ]
  147. *
  148. * otherRange = new Range( new Position( [ 3 ], root ), new Position( [ 4 ], root ) );
  149. * transformed = range.getDifference( otherRange );
  150. * // transformed array has two ranges: from [ 2, 7 ] to [ 3 ] and from [ 4 ] to [ 4, 0, 1 ]
  151. *
  152. * @param {document.Range} otherRange Range to differentiate against.
  153. * @returns {Array.<document.Range>} The difference between ranges.
  154. */
  155. getDifference( otherRange ) {
  156. const ranges = [];
  157. if ( this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start ) ) {
  158. // Ranges intersect.
  159. if ( this.containsPosition( otherRange.start ) ) {
  160. // Given range start is inside this range. This means that we have to
  161. // add shrunken range - from the start to the middle of this range.
  162. ranges.push(
  163. new Range(
  164. this.start.clone(),
  165. otherRange.start.clone()
  166. )
  167. );
  168. }
  169. if ( this.containsPosition( otherRange.end ) ) {
  170. // Given range end is inside this range. This means that we have to
  171. // add shrunken range - from the middle of this range to the end.
  172. ranges.push(
  173. new Range(
  174. otherRange.end.clone(),
  175. this.end.clone()
  176. )
  177. );
  178. }
  179. } else {
  180. // Ranges do not intersect, return the original range.
  181. ranges.push( this.clone() );
  182. }
  183. return ranges;
  184. }
  185. /**
  186. * Returns an intersection of this {document.Range range} and given {document.Range range}. Intersection
  187. * is a common part of both of those ranges. If ranges has no common part, returns null.
  188. *
  189. * Examples:
  190. * let range = new Range( new Position( [ 2, 7 ], root ), new Position( [ 4, 0, 1 ], root ) );
  191. * let otherRange = new Range( new Position( [ 1 ], root ), new Position( [ 2 ], root ) );
  192. * let transformed = range.getIntersection( otherRange ); // null - ranges have no common part
  193. *
  194. * otherRange = new Range( new Position( [ 3 ], root ), new Position( [ 5 ], root ) );
  195. * transformed = range.getIntersection( otherRange ); // range from [ 3 ] to [ 4, 0, 1 ]
  196. *
  197. * @param {document.Range} otherRange Range to check for intersection.
  198. * @returns {document.Range|null} A common part of given ranges or null if ranges have no common part.
  199. */
  200. getIntersection( otherRange ) {
  201. if ( this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start ) ) {
  202. // Ranges intersect, so a common range will be returned.
  203. // At most, it will be same as this range.
  204. let commonRangeStart = this.start;
  205. let commonRangeEnd = this.end;
  206. if ( this.containsPosition( otherRange.start ) ) {
  207. // Given range start is inside this range. This means thaNt we have to
  208. // shrink common range to the given range start.
  209. commonRangeStart = otherRange.start;
  210. }
  211. if ( this.containsPosition( otherRange.end ) ) {
  212. // Given range end is inside this range. This means that we have to
  213. // shrink common range to the given range end.
  214. commonRangeEnd = otherRange.end;
  215. }
  216. return new Range( commonRangeStart.clone(), commonRangeEnd.clone() );
  217. }
  218. // Ranges do not intersect, so they do not have common part.
  219. return null;
  220. }
  221. /**
  222. * Creates a new range spreading from specified position to the same position moved by given offset.
  223. *
  224. * @param {document.Position} position Beginning of the range.
  225. * @param {Number} offset How long the range should be.
  226. * @returns {document.Range}
  227. */
  228. static createFromPositionAndOffset( position, offset ) {
  229. let endPosition = position.clone();
  230. endPosition.offset += offset;
  231. return new Range( position, endPosition );
  232. }
  233. /**
  234. * Range iterator.
  235. *
  236. * @see document.PositionIterator
  237. */
  238. [ Symbol.iterator ]() {
  239. return new PositionIterator( this );
  240. }
  241. }
  242. return Range;
  243. } );