8
0

range.js 17 KB

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