range.js 12 KB

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