range.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/view/range
  7. */
  8. import Text from './text';
  9. import Position from './position';
  10. import TreeWalker from './treewalker';
  11. import AttributeElement from './attributeelement';
  12. import ContainerElement from './containerelement';
  13. import UIElement from './uielement';
  14. /**
  15. * Tree view range.
  16. */
  17. export default class Range {
  18. /**
  19. * Creates a range spanning from `start` position to `end` position.
  20. *
  21. * **Note:** Constructor creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
  22. *
  23. * @param {module:engine/view/position~Position} start Start position.
  24. * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
  25. */
  26. constructor( start, end = null ) {
  27. /**
  28. * Start position.
  29. *
  30. * @member {module:engine/view/position~Position}
  31. */
  32. this.start = Position.createFromPosition( start );
  33. /**
  34. * End position.
  35. *
  36. * @member {module:engine/view/position~Position}
  37. */
  38. this.end = end ? Position.createFromPosition( end ) : Position.createFromPosition( start );
  39. }
  40. /**
  41. * Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
  42. * them together with additional information like length or {@link module:engine/view/position~Position positions},
  43. * grouped as {@link module:engine/view/treewalker~TreeWalkerValue}.
  44. *
  45. * This iterator uses {@link module:engine/view/treewalker~TreeWalker TreeWalker} with `boundaries` set to this range and
  46. * `ignoreElementEnd` option
  47. * set to `true`.
  48. *
  49. * @returns {Iterable.<module:engine/view/treewalker~TreeWalkerValue>}
  50. */
  51. *[ Symbol.iterator ]() {
  52. yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );
  53. }
  54. /**
  55. * Returns whether the range is collapsed, that is it start and end positions are equal.
  56. *
  57. * @type {Boolean}
  58. */
  59. get isCollapsed() {
  60. return this.start.isEqual( this.end );
  61. }
  62. /**
  63. * Returns whether this range is flat, that is if {@link module:engine/view/range~Range#start start} position and
  64. * {@link module:engine/view/range~Range#end end} position are in the same {@link module:engine/view/position~Position#parent parent}.
  65. *
  66. * @type {Boolean}
  67. */
  68. get isFlat() {
  69. return this.start.parent === this.end.parent;
  70. }
  71. /**
  72. * Range root element.
  73. *
  74. * @type {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
  75. */
  76. get root() {
  77. return this.start.root;
  78. }
  79. /**
  80. * Creates a maximal range that has the same content as this range but is expanded in both ways (at the beginning
  81. * and at the end).
  82. *
  83. * For example:
  84. *
  85. * <p>Foo</p><p><b>{Bar}</b></p> -> <p>Foo</p>[<p><b>Bar</b></p>]
  86. * <p><b>foo</b>{bar}<span></span></p> -> <p><b>foo[</b>bar<span></span>]</p>
  87. *
  88. * Note that in the sample above:
  89. * - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
  90. * - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
  91. * - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
  92. *
  93. * @returns {module:engine/view/range~Range} Enlarged range.
  94. */
  95. getEnlarged() {
  96. const start = this.start.getLastMatchingPosition( enlargeShrinkStartSkip, { direction: 'backward' } );
  97. const end = this.end.getLastMatchingPosition( enlargeShrinkEndSkip );
  98. return new Range( start, end );
  99. }
  100. /**
  101. * Creates a minimum range that has the same content as this range but is trimmed in both ways (at the beginning
  102. * and at the end).
  103. *
  104. * For example:
  105. *
  106. * <p>Foo</p>[<p><b>Bar</b></p>] -> <p>Foo</p><p><b>{Bar}</b></p>
  107. * <p><b>foo[</b>bar<span></span>]</p> -> <p><b>foo</b>{bar}<span></span></p>
  108. *
  109. * Note that in the sample above:
  110. * - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
  111. * - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
  112. * - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
  113. *
  114. * @returns {module:engine/view/range~Range} Shrink range.
  115. */
  116. getTrimmed() {
  117. let start = this.start.getLastMatchingPosition( enlargeShrinkStartSkip );
  118. let end = this.end.getLastMatchingPosition( enlargeShrinkEndSkip, { direction: 'backward' } );
  119. let nodeAfterStart = start.nodeAfter;
  120. let nodeBeforeEnd = end.nodeBefore;
  121. // Because TreeWalker prefers positions next to text node, we need to move them manually into these text nodes.
  122. if ( nodeAfterStart instanceof Text ) {
  123. start = new Position( nodeAfterStart, 0 );
  124. }
  125. if ( nodeBeforeEnd instanceof Text ) {
  126. end = new Position( nodeBeforeEnd, nodeBeforeEnd.data.length );
  127. }
  128. return new Range( start, end );
  129. }
  130. /**
  131. * Two ranges are equal if their start and end positions are equal.
  132. *
  133. * @param {module:engine/view/range~Range} otherRange Range to compare with.
  134. * @returns {Boolean} `true` if ranges are equal, `false` otherwise
  135. */
  136. isEqual( otherRange ) {
  137. return this == otherRange || ( this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end ) );
  138. }
  139. /**
  140. * Checks whether this range contains given {@link module:engine/view/position~Position position}.
  141. *
  142. * @param {module:engine/view/position~Position} position Position to check.
  143. * @returns {Boolean} `true` if given {@link module:engine/view/position~Position position} is contained in this range, `false` otherwise.
  144. */
  145. containsPosition( position ) {
  146. return position.isAfter( this.start ) && position.isBefore( this.end );
  147. }
  148. /**
  149. * Checks whether this range contains given {@link module:engine/view/range~Range range}.
  150. *
  151. * @param {module:engine/view/range~Range} otherRange Range to check.
  152. * @returns {Boolean} `true` if given {@link module:engine/view/range~Range range} boundaries are contained by this range, `false`
  153. * otherwise.
  154. */
  155. containsRange( otherRange ) {
  156. return this.containsPosition( otherRange.start ) && this.containsPosition( otherRange.end );
  157. }
  158. /**
  159. * Computes which part(s) of this {@link module:engine/view/range~Range range} is not a part of given
  160. * {@link module:engine/view/range~Range range}.
  161. * Returned array contains zero, one or two {@link module:engine/view/range~Range ranges}.
  162. *
  163. * Examples:
  164. *
  165. * let foo = new Text( 'foo' );
  166. * let img = new ContainerElement( 'img' );
  167. * let bar = new Text( 'bar' );
  168. * let p = new ContainerElement( 'p', null, [ foo, img, bar ] );
  169. *
  170. * let range = new Range( new Position( foo, 2 ), new Position( bar, 1 ); // "o", img, "b" are in range.
  171. * let otherRange = new Range( new Position( foo, 1 ), new Position( bar, 2 ); "oo", img, "ba" are in range.
  172. * let transformed = range.getDifference( otherRange );
  173. * // transformed array has no ranges because `otherRange` contains `range`
  174. *
  175. * otherRange = new Range( new Position( foo, 1 ), new Position( p, 2 ); // "oo", img are in range.
  176. * transformed = range.getDifference( otherRange );
  177. * // transformed array has one range: from ( p, 2 ) to ( bar, 1 )
  178. *
  179. * otherRange = new Range( new Position( p, 1 ), new Position( p, 2 ) ); // img is in range.
  180. * transformed = range.getDifference( otherRange );
  181. * // transformed array has two ranges: from ( foo, 1 ) to ( p, 1 ) and from ( p, 2 ) to ( bar, 1 )
  182. *
  183. * @param {module:engine/view/range~Range} otherRange Range to differentiate against.
  184. * @returns {Array.<module:engine/view/range~Range>} The difference between ranges.
  185. */
  186. getDifference( otherRange ) {
  187. const ranges = [];
  188. if ( this.isIntersecting( otherRange ) ) {
  189. // Ranges intersect.
  190. if ( this.containsPosition( otherRange.start ) ) {
  191. // Given range start is inside this range. This means that we have to
  192. // add shrunken range - from the start to the middle of this range.
  193. ranges.push( new Range( this.start, otherRange.start ) );
  194. }
  195. if ( this.containsPosition( otherRange.end ) ) {
  196. // Given range end is inside this range. This means that we have to
  197. // add shrunken range - from the middle of this range to the end.
  198. ranges.push( new Range( otherRange.end, this.end ) );
  199. }
  200. } else {
  201. // Ranges do not intersect, return the original range.
  202. ranges.push( Range.createFromRange( this ) );
  203. }
  204. return ranges;
  205. }
  206. /**
  207. * Returns an intersection of this {@link module:engine/view/range~Range range} and given {@link module:engine/view/range~Range range}.
  208. * Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`.
  209. *
  210. * Examples:
  211. *
  212. * let foo = new Text( 'foo' );
  213. * let img = new ContainerElement( 'img' );
  214. * let bar = new Text( 'bar' );
  215. * let p = new ContainerElement( 'p', null, [ foo, img, bar ] );
  216. *
  217. * let range = new Range( new Position( foo, 2 ), new Position( bar, 1 ); // "o", img, "b" are in range.
  218. * let otherRange = new Range( new Position( foo, 1 ), new Position( p, 2 ); // "oo", img are in range.
  219. * let transformed = range.getIntersection( otherRange ); // range from ( foo, 1 ) to ( p, 2 ).
  220. *
  221. * otherRange = new Range( new Position( bar, 1 ), new Position( bar, 3 ); "ar" is in range.
  222. * transformed = range.getIntersection( otherRange ); // null - no common part.
  223. *
  224. * @param {module:engine/view/range~Range} otherRange Range to check for intersection.
  225. * @returns {module:engine/view/range~Range|null} A common part of given ranges or `null` if ranges have no common part.
  226. */
  227. getIntersection( otherRange ) {
  228. if ( this.isIntersecting( otherRange ) ) {
  229. // Ranges intersect, so a common range will be returned.
  230. // At most, it will be same as this range.
  231. let commonRangeStart = this.start;
  232. let commonRangeEnd = this.end;
  233. if ( this.containsPosition( otherRange.start ) ) {
  234. // Given range start is inside this range. This means thaNt we have to
  235. // shrink common range to the given range start.
  236. commonRangeStart = otherRange.start;
  237. }
  238. if ( this.containsPosition( otherRange.end ) ) {
  239. // Given range end is inside this range. This means that we have to
  240. // shrink common range to the given range end.
  241. commonRangeEnd = otherRange.end;
  242. }
  243. return new Range( commonRangeStart, commonRangeEnd );
  244. }
  245. // Ranges do not intersect, so they do not have common part.
  246. return null;
  247. }
  248. /**
  249. * Creates a {@link module:engine/view/treewalker~TreeWalker TreeWalker} instance with this range as a boundary.
  250. *
  251. * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
  252. * @param {module:engine/view/position~Position} [options.startPosition]
  253. * @param {Boolean} [options.singleCharacters=false]
  254. * @param {Boolean} [options.shallow=false]
  255. * @param {Boolean} [options.ignoreElementEnd=false]
  256. */
  257. getWalker( options = {} ) {
  258. options.boundaries = this;
  259. return new TreeWalker( options );
  260. }
  261. /**
  262. * Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
  263. * them.
  264. *
  265. * This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option
  266. * set to `true`. However it returns only {@link module:engine/view/item~Item items},
  267. * not {@link module:engine/view/treewalker~TreeWalkerValue}.
  268. *
  269. * You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for
  270. * a full list of available options.
  271. *
  272. * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
  273. * @returns {Iterable.<module:engine/view/item~Item>}
  274. */
  275. *getItems( options = {} ) {
  276. options.boundaries = this;
  277. options.ignoreElementEnd = true;
  278. const treeWalker = new TreeWalker( options );
  279. for ( let value of treeWalker ) {
  280. yield value.item;
  281. }
  282. }
  283. /**
  284. * Returns an iterator that iterates over all {@link module:engine/view/position~Position positions} that are boundaries or
  285. * contained in this range.
  286. *
  287. * This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range. However it returns only
  288. * {@link module:engine/view/position~Position positions}, not {@link module:engine/view/treewalker~TreeWalkerValue}.
  289. *
  290. * You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for
  291. * a full list of available options.
  292. *
  293. * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
  294. * @returns {Iterable.<module:engine/view/position~Position>}
  295. */
  296. *getPositions( options = {} ) {
  297. options.boundaries = this;
  298. const treeWalker = new TreeWalker( options );
  299. yield treeWalker.position;
  300. for ( let value of treeWalker ) {
  301. yield value.nextPosition;
  302. }
  303. }
  304. /**
  305. * Checks and returns whether this range intersects with given range.
  306. *
  307. * @param {module:engine/view/range~Range} otherRange Range to compare with.
  308. * @returns {Boolean} True if ranges intersect.
  309. */
  310. isIntersecting( otherRange ) {
  311. return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start );
  312. }
  313. /**
  314. * Creates a range from given parents and offsets.
  315. *
  316. * @param {module:engine/view/element~Element} startElement Start position parent element.
  317. * @param {Number} startOffset Start position offset.
  318. * @param {module:engine/view/element~Element} endElement End position parent element.
  319. * @param {Number} endOffset End position offset.
  320. * @returns {module:engine/view/range~Range} Created range.
  321. */
  322. static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
  323. return new this(
  324. new Position( startElement, startOffset ),
  325. new Position( endElement, endOffset )
  326. );
  327. }
  328. /**
  329. * Creates and returns a new instance of Range which is equal to passed range.
  330. *
  331. * @param {module:engine/view/range~Range} range Range to clone.
  332. * @returns {module:engine/view/range~Range}
  333. */
  334. static createFromRange( range ) {
  335. return new this( range.start, range.end );
  336. }
  337. /**
  338. * Creates a new range, spreading from specified {@link module:engine/view/position~Position position} to a position moved by
  339. * given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range.
  340. *
  341. * @param {module:engine/view/position~Position} position Beginning of the range.
  342. * @param {Number} shift How long the range should be.
  343. * @returns {module:engine/view/range~Range}
  344. */
  345. static createFromPositionAndShift( position, shift ) {
  346. const start = position;
  347. const end = position.getShiftedBy( shift );
  348. return shift > 0 ? new this( start, end ) : new this( end, start );
  349. }
  350. /**
  351. * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
  352. * that element and ends after the last child of that element.
  353. *
  354. * @param {module:engine/view/element~Element} element Element which is a parent for the range.
  355. * @returns {module:engine/view/range~Range}
  356. */
  357. static createIn( element ) {
  358. return this.createFromParentsAndOffsets( element, 0, element, element.childCount );
  359. }
  360. /**
  361. * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
  362. *
  363. * @param {module:engine/view/item~Item} item
  364. * @returns {module:engine/view/range~Range}
  365. */
  366. static createOn( item ) {
  367. return this.createFromPositionAndShift( Position.createBefore( item ), 1 );
  368. }
  369. }
  370. // Function used by getEnlagred and getShrinked methods.
  371. function enlargeShrinkStartSkip( value ) {
  372. if ( value.item instanceof AttributeElement || value.item instanceof UIElement ) {
  373. return true;
  374. }
  375. if ( value.item instanceof ContainerElement && value.type == 'elementStart' ) {
  376. return true;
  377. }
  378. return false;
  379. }
  380. // Function used by getEnlagred and getShrinked methods.
  381. function enlargeShrinkEndSkip( value ) {
  382. if ( value.item instanceof AttributeElement || value.item instanceof UIElement ) {
  383. return true;
  384. }
  385. if ( value.item instanceof ContainerElement && value.type == 'elementEnd' ) {
  386. return true;
  387. }
  388. return false;
  389. }