range.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module engine/view/range
  7. */
  8. import Position from './position';
  9. import TreeWalker from './treewalker';
  10. /**
  11. * Range in the view tree. A range is represented by its start and end {@link module:engine/view/position~Position positions}.
  12. *
  13. * In order to create a new position instance use the `createPosition*()` factory methods available in:
  14. *
  15. * * {@link module:engine/view/view~View}
  16. * * {@link module:engine/view/downcastwriter~DowncastWriter}
  17. * * {@link module:engine/view/upcastwriter~UpcastWriter}
  18. */
  19. export default class Range {
  20. /**
  21. * Creates a range spanning from `start` position to `end` position.
  22. *
  23. * **Note:** Constructor creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
  24. *
  25. * @param {module:engine/view/position~Position} start Start position.
  26. * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at the `start` position.
  27. */
  28. constructor( start, end = null ) {
  29. /**
  30. * Start position.
  31. *
  32. * @readonly
  33. * @member {module:engine/view/position~Position}
  34. */
  35. this.start = start.clone();
  36. /**
  37. * End position.
  38. *
  39. * @readonly
  40. * @member {module:engine/view/position~Position}
  41. */
  42. this.end = end ? end.clone() : start.clone();
  43. }
  44. /**
  45. * Iterable interface.
  46. *
  47. * Iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
  48. * them together with additional information like length or {@link module:engine/view/position~Position positions},
  49. * grouped as {@link module:engine/view/treewalker~TreeWalkerValue}.
  50. *
  51. * This iterator uses {@link module:engine/view/treewalker~TreeWalker TreeWalker} with `boundaries` set to this range and
  52. * `ignoreElementEnd` option
  53. * set to `true`.
  54. *
  55. * @returns {Iterable.<module:engine/view/treewalker~TreeWalkerValue>}
  56. */
  57. * [ Symbol.iterator ]() {
  58. yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );
  59. }
  60. /**
  61. * Returns whether the range is collapsed, that is it start and end positions are equal.
  62. *
  63. * @type {Boolean}
  64. */
  65. get isCollapsed() {
  66. return this.start.isEqual( this.end );
  67. }
  68. /**
  69. * Returns whether this range is flat, that is if {@link module:engine/view/range~Range#start start} position and
  70. * {@link module:engine/view/range~Range#end end} position are in the same {@link module:engine/view/position~Position#parent parent}.
  71. *
  72. * @type {Boolean}
  73. */
  74. get isFlat() {
  75. return this.start.parent === this.end.parent;
  76. }
  77. /**
  78. * Range root element.
  79. *
  80. * @type {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
  81. */
  82. get root() {
  83. return this.start.root;
  84. }
  85. /**
  86. * Creates a maximal range that has the same content as this range but is expanded in both ways (at the beginning
  87. * and at the end).
  88. *
  89. * For example:
  90. *
  91. * <p>Foo</p><p><b>{Bar}</b></p> -> <p>Foo</p>[<p><b>Bar</b>]</p>
  92. * <p><b>foo</b>{bar}<span></span></p> -> <p><b>foo[</b>bar<span></span>]</p>
  93. *
  94. * Note that in the sample above:
  95. *
  96. * - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
  97. * - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
  98. * - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
  99. *
  100. * @returns {module:engine/view/range~Range} Enlarged range.
  101. */
  102. getEnlarged() {
  103. let start = this.start.getLastMatchingPosition( enlargeTrimSkip, { direction: 'backward' } );
  104. let end = this.end.getLastMatchingPosition( enlargeTrimSkip );
  105. // Fix positions, in case if they are in Text node.
  106. if ( start.parent.is( '$text' ) && start.isAtStart ) {
  107. start = Position._createBefore( start.parent );
  108. }
  109. if ( end.parent.is( '$text' ) && end.isAtEnd ) {
  110. end = Position._createAfter( end.parent );
  111. }
  112. return new Range( start, end );
  113. }
  114. /**
  115. * Creates a minimum range that has the same content as this range but is trimmed in both ways (at the beginning
  116. * and at the end).
  117. *
  118. * For example:
  119. *
  120. * <p>Foo</p>[<p><b>Bar</b>]</p> -> <p>Foo</p><p><b>{Bar}</b></p>
  121. * <p><b>foo[</b>bar<span></span>]</p> -> <p><b>foo</b>{bar}<span></span></p>
  122. *
  123. * Note that in the sample above:
  124. *
  125. * - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
  126. * - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
  127. * - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
  128. *
  129. * @returns {module:engine/view/range~Range} Shrink range.
  130. */
  131. getTrimmed() {
  132. let start = this.start.getLastMatchingPosition( enlargeTrimSkip );
  133. if ( start.isAfter( this.end ) || start.isEqual( this.end ) ) {
  134. return new Range( start, start );
  135. }
  136. let end = this.end.getLastMatchingPosition( enlargeTrimSkip, { direction: 'backward' } );
  137. const nodeAfterStart = start.nodeAfter;
  138. const nodeBeforeEnd = end.nodeBefore;
  139. // Because TreeWalker prefers positions next to text node, we need to move them manually into these text nodes.
  140. if ( nodeAfterStart && nodeAfterStart.is( '$text' ) ) {
  141. start = new Position( nodeAfterStart, 0 );
  142. }
  143. if ( nodeBeforeEnd && nodeBeforeEnd.is( '$text' ) ) {
  144. end = new Position( nodeBeforeEnd, nodeBeforeEnd.data.length );
  145. }
  146. return new Range( start, end );
  147. }
  148. /**
  149. * Two ranges are equal if their start and end positions are equal.
  150. *
  151. * @param {module:engine/view/range~Range} otherRange Range to compare with.
  152. * @returns {Boolean} `true` if ranges are equal, `false` otherwise
  153. */
  154. isEqual( otherRange ) {
  155. return this == otherRange || ( this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end ) );
  156. }
  157. /**
  158. * Checks whether this range contains given {@link module:engine/view/position~Position position}.
  159. *
  160. * @param {module:engine/view/position~Position} position Position to check.
  161. * @returns {Boolean} `true` if given {@link module:engine/view/position~Position position} is contained in this range,
  162. * `false` otherwise.
  163. */
  164. containsPosition( position ) {
  165. return position.isAfter( this.start ) && position.isBefore( this.end );
  166. }
  167. /**
  168. * Checks whether this range contains given {@link module:engine/view/range~Range range}.
  169. *
  170. * @param {module:engine/view/range~Range} otherRange Range to check.
  171. * @param {Boolean} [loose=false] Whether the check is loose or strict. If the check is strict (`false`), compared range cannot
  172. * start or end at the same position as this range boundaries. If the check is loose (`true`), compared range can start, end or
  173. * even be equal to this range. Note that collapsed ranges are always compared in strict mode.
  174. * @returns {Boolean} `true` if given {@link module:engine/view/range~Range range} boundaries are contained by this range, `false`
  175. * otherwise.
  176. */
  177. containsRange( otherRange, loose = false ) {
  178. if ( otherRange.isCollapsed ) {
  179. loose = false;
  180. }
  181. const containsStart = this.containsPosition( otherRange.start ) || ( loose && this.start.isEqual( otherRange.start ) );
  182. const containsEnd = this.containsPosition( otherRange.end ) || ( loose && this.end.isEqual( otherRange.end ) );
  183. return containsStart && containsEnd;
  184. }
  185. /**
  186. * Computes which part(s) of this {@link module:engine/view/range~Range range} is not a part of given
  187. * {@link module:engine/view/range~Range range}.
  188. * Returned array contains zero, one or two {@link module:engine/view/range~Range ranges}.
  189. *
  190. * Examples:
  191. *
  192. * let foo = downcastWriter.createText( 'foo' );
  193. * let img = downcastWriter.createContainerElement( 'img' );
  194. * let bar = downcastWriter.createText( 'bar' );
  195. * let p = downcastWriter.createContainerElement( 'p', null, [ foo, img, bar ] );
  196. *
  197. * let range = view.createRange( view.createPositionAt( foo, 2 ), view.createPositionAt( bar, 1 ); // "o", img, "b" are in range.
  198. * let otherRange = view.createRange( // "oo", img, "ba" are in range.
  199. * view.createPositionAt( foo, 1 ),
  200. * view.createPositionAt( bar, 2 )
  201. * );
  202. * let transformed = range.getDifference( otherRange );
  203. * // transformed array has no ranges because `otherRange` contains `range`
  204. *
  205. * otherRange = view.createRange( view.createPositionAt( foo, 1 ), view.createPositionAt( p, 2 ); // "oo", img are in range.
  206. * transformed = range.getDifference( otherRange );
  207. * // transformed array has one range: from ( p, 2 ) to ( bar, 1 )
  208. *
  209. * otherRange = view.createRange( view.createPositionAt( p, 1 ), view.createPositionAt( p, 2 ) ); // img is in range.
  210. * transformed = range.getDifference( otherRange );
  211. * // transformed array has two ranges: from ( foo, 1 ) to ( p, 1 ) and from ( p, 2 ) to ( bar, 1 )
  212. *
  213. * @param {module:engine/view/range~Range} otherRange Range to differentiate against.
  214. * @returns {Array.<module:engine/view/range~Range>} The difference between ranges.
  215. */
  216. getDifference( otherRange ) {
  217. const ranges = [];
  218. if ( this.isIntersecting( otherRange ) ) {
  219. // Ranges intersect.
  220. if ( this.containsPosition( otherRange.start ) ) {
  221. // Given range start is inside this range. This means that we have to
  222. // add shrunken range - from the start to the middle of this range.
  223. ranges.push( new Range( this.start, otherRange.start ) );
  224. }
  225. if ( this.containsPosition( otherRange.end ) ) {
  226. // Given range end is inside this range. This means that we have to
  227. // add shrunken range - from the middle of this range to the end.
  228. ranges.push( new Range( otherRange.end, this.end ) );
  229. }
  230. } else {
  231. // Ranges do not intersect, return the original range.
  232. ranges.push( this.clone() );
  233. }
  234. return ranges;
  235. }
  236. /**
  237. * Returns an intersection of this {@link module:engine/view/range~Range range} and given {@link module:engine/view/range~Range range}.
  238. * Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`.
  239. *
  240. * Examples:
  241. *
  242. * let foo = downcastWriter.createText( 'foo' );
  243. * let img = downcastWriter.createContainerElement( 'img' );
  244. * let bar = downcastWriter.createText( 'bar' );
  245. * let p = downcastWriter.createContainerElement( 'p', null, [ foo, img, bar ] );
  246. *
  247. * let range = view.createRange( view.createPositionAt( foo, 2 ), view.createPositionAt( bar, 1 ); // "o", img, "b" are in range.
  248. * let otherRange = view.createRange( view.createPositionAt( foo, 1 ), view.createPositionAt( p, 2 ); // "oo", img are in range.
  249. * let transformed = range.getIntersection( otherRange ); // range from ( foo, 1 ) to ( p, 2 ).
  250. *
  251. * otherRange = view.createRange( view.createPositionAt( bar, 1 ), view.createPositionAt( bar, 3 ); "ar" is in range.
  252. * transformed = range.getIntersection( otherRange ); // null - no common part.
  253. *
  254. * @param {module:engine/view/range~Range} otherRange Range to check for intersection.
  255. * @returns {module:engine/view/range~Range|null} A common part of given ranges or `null` if ranges have no common part.
  256. */
  257. getIntersection( otherRange ) {
  258. if ( this.isIntersecting( otherRange ) ) {
  259. // Ranges intersect, so a common range will be returned.
  260. // At most, it will be same as this range.
  261. let commonRangeStart = this.start;
  262. let commonRangeEnd = this.end;
  263. if ( this.containsPosition( otherRange.start ) ) {
  264. // Given range start is inside this range. This means thaNt we have to
  265. // shrink common range to the given range start.
  266. commonRangeStart = otherRange.start;
  267. }
  268. if ( this.containsPosition( otherRange.end ) ) {
  269. // Given range end is inside this range. This means that we have to
  270. // shrink common range to the given range end.
  271. commonRangeEnd = otherRange.end;
  272. }
  273. return new Range( commonRangeStart, commonRangeEnd );
  274. }
  275. // Ranges do not intersect, so they do not have common part.
  276. return null;
  277. }
  278. /**
  279. * Creates a {@link module:engine/view/treewalker~TreeWalker TreeWalker} instance with this range as a boundary.
  280. *
  281. * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
  282. * @param {module:engine/view/position~Position} [options.startPosition]
  283. * @param {Boolean} [options.singleCharacters=false]
  284. * @param {Boolean} [options.shallow=false]
  285. * @param {Boolean} [options.ignoreElementEnd=false]
  286. * @returns {module:engine/view/treewalker~TreeWalker}
  287. */
  288. getWalker( options = {} ) {
  289. options.boundaries = this;
  290. return new TreeWalker( options );
  291. }
  292. /**
  293. * Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment}
  294. * which is a common ancestor of range's both ends (in which the entire range is contained).
  295. *
  296. * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null}
  297. */
  298. getCommonAncestor() {
  299. return this.start.getCommonAncestor( this.end );
  300. }
  301. /**
  302. * Returns an {@link module:engine/view/element~Element Element} contained by the range.
  303. * The element will be returned when it is the **only** node within the range and **fully–contained**
  304. * at the same time.
  305. *
  306. * @returns {module:engine/view/element~Element|null}
  307. */
  308. getContainedElement() {
  309. if ( this.isCollapsed ) {
  310. return null;
  311. }
  312. let nodeAfterStart = this.start.nodeAfter;
  313. let nodeBeforeEnd = this.end.nodeBefore;
  314. // Handle the situation when the range position is at the beginning / at the end of a text node.
  315. // In such situation `.nodeAfter` and `.nodeBefore` are `null` but the range still might be spanning
  316. // over one element.
  317. //
  318. // <p>Foo{<span class="widget"></span>}bar</p> vs <p>Foo[<span class="widget"></span>]bar</p>
  319. //
  320. // These are basically the same range, only the difference is if the range position is at
  321. // at the end/at the beginning of a text node or just before/just after the text node.
  322. //
  323. if ( this.start.parent.is( '$text' ) && this.start.isAtEnd && this.start.parent.nextSibling ) {
  324. nodeAfterStart = this.start.parent.nextSibling;
  325. }
  326. if ( this.end.parent.is( '$text' ) && this.end.isAtStart && this.end.parent.previousSibling ) {
  327. nodeBeforeEnd = this.end.parent.previousSibling;
  328. }
  329. if ( nodeAfterStart && nodeAfterStart.is( 'element' ) && nodeAfterStart === nodeBeforeEnd ) {
  330. return nodeAfterStart;
  331. }
  332. return null;
  333. }
  334. /**
  335. * Clones this range.
  336. *
  337. * @returns {module:engine/view/range~Range}
  338. */
  339. clone() {
  340. return new Range( this.start, this.end );
  341. }
  342. /**
  343. * Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
  344. * them.
  345. *
  346. * This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option
  347. * set to `true`. However it returns only {@link module:engine/view/item~Item items},
  348. * not {@link module:engine/view/treewalker~TreeWalkerValue}.
  349. *
  350. * You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for
  351. * a full list of available options.
  352. *
  353. * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
  354. * @returns {Iterable.<module:engine/view/item~Item>}
  355. */
  356. * getItems( options = {} ) {
  357. options.boundaries = this;
  358. options.ignoreElementEnd = true;
  359. const treeWalker = new TreeWalker( options );
  360. for ( const value of treeWalker ) {
  361. yield value.item;
  362. }
  363. }
  364. /**
  365. * Returns an iterator that iterates over all {@link module:engine/view/position~Position positions} that are boundaries or
  366. * contained in this range.
  367. *
  368. * This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range. However it returns only
  369. * {@link module:engine/view/position~Position positions}, not {@link module:engine/view/treewalker~TreeWalkerValue}.
  370. *
  371. * You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for
  372. * a full list of available options.
  373. *
  374. * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
  375. * @returns {Iterable.<module:engine/view/position~Position>}
  376. */
  377. * getPositions( options = {} ) {
  378. options.boundaries = this;
  379. const treeWalker = new TreeWalker( options );
  380. yield treeWalker.position;
  381. for ( const value of treeWalker ) {
  382. yield value.nextPosition;
  383. }
  384. }
  385. /**
  386. * Checks whether this object is of the given type.
  387. *
  388. * range.is( 'range' ); // -> true
  389. * range.is( 'view:range' ); // -> true
  390. *
  391. * range.is( 'model:range' ); // -> false
  392. * range.is( 'element' ); // -> false
  393. * range.is( 'selection' ); // -> false
  394. *
  395. * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
  396. *
  397. * @param {String} type
  398. * @returns {Boolean}
  399. */
  400. is( type ) {
  401. return type === 'range' || type === 'view:range';
  402. }
  403. /**
  404. * Checks and returns whether this range intersects with the given range.
  405. *
  406. * @param {module:engine/view/range~Range} otherRange Range to compare with.
  407. * @returns {Boolean} True if ranges intersect.
  408. */
  409. isIntersecting( otherRange ) {
  410. return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start );
  411. }
  412. /**
  413. * Creates a range from the given parents and offsets.
  414. *
  415. * @protected
  416. * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} startElement Start position
  417. * parent element.
  418. * @param {Number} startOffset Start position offset.
  419. * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} endElement End position
  420. * parent element.
  421. * @param {Number} endOffset End position offset.
  422. * @returns {module:engine/view/range~Range} Created range.
  423. */
  424. static _createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
  425. return new this(
  426. new Position( startElement, startOffset ),
  427. new Position( endElement, endOffset )
  428. );
  429. }
  430. /**
  431. * Creates a new range, spreading from specified {@link module:engine/view/position~Position position} to a position moved by
  432. * given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range.
  433. *
  434. * @protected
  435. * @param {module:engine/view/position~Position} position Beginning of the range.
  436. * @param {Number} shift How long the range should be.
  437. * @returns {module:engine/view/range~Range}
  438. */
  439. static _createFromPositionAndShift( position, shift ) {
  440. const start = position;
  441. const end = position.getShiftedBy( shift );
  442. return shift > 0 ? new this( start, end ) : new this( end, start );
  443. }
  444. /**
  445. * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
  446. * that element and ends after the last child of that element.
  447. *
  448. * @protected
  449. * @param {module:engine/view/element~Element} element Element which is a parent for the range.
  450. * @returns {module:engine/view/range~Range}
  451. */
  452. static _createIn( element ) {
  453. return this._createFromParentsAndOffsets( element, 0, element, element.childCount );
  454. }
  455. /**
  456. * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
  457. *
  458. * @protected
  459. * @param {module:engine/view/item~Item} item
  460. * @returns {module:engine/view/range~Range}
  461. */
  462. static _createOn( item ) {
  463. const size = item.is( '$textProxy' ) ? item.offsetSize : 1;
  464. return this._createFromPositionAndShift( Position._createBefore( item ), size );
  465. }
  466. }
  467. // Function used by getEnlarged and getTrimmed methods.
  468. function enlargeTrimSkip( value ) {
  469. if ( value.item.is( 'attributeElement' ) || value.item.is( 'uiElement' ) ) {
  470. return true;
  471. }
  472. return false;
  473. }