range.js 17 KB

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