8
0

range.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  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( enlargeShrinkSkip, { direction: 'backward' } );
  93. let end = this.end.getLastMatchingPosition( enlargeShrinkSkip );
  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( enlargeShrinkSkip );
  121. let end = this.end.getLastMatchingPosition( enlargeShrinkSkip, { direction: 'backward' } );
  122. const nodeAfterStart = start.nodeAfter;
  123. const nodeBeforeEnd = end.nodeBefore;
  124. // Because TreeWalker prefers positions next to text node, we need to move them manually into these text nodes.
  125. if ( nodeAfterStart && nodeAfterStart.is( 'text' ) ) {
  126. start = new Position( nodeAfterStart, 0 );
  127. }
  128. if ( nodeBeforeEnd && nodeBeforeEnd.is( 'text' ) ) {
  129. end = new Position( nodeBeforeEnd, nodeBeforeEnd.data.length );
  130. }
  131. return new Range( start, end );
  132. }
  133. /**
  134. * Two ranges are equal if their start and end positions are equal.
  135. *
  136. * @param {module:engine/view/range~Range} otherRange Range to compare with.
  137. * @returns {Boolean} `true` if ranges are equal, `false` otherwise
  138. */
  139. isEqual( otherRange ) {
  140. return this == otherRange || ( this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end ) );
  141. }
  142. /**
  143. * Checks whether this range contains given {@link module:engine/view/position~Position position}.
  144. *
  145. * @param {module:engine/view/position~Position} position Position to check.
  146. * @returns {Boolean} `true` if given {@link module:engine/view/position~Position position} is contained in this range,
  147. * `false` otherwise.
  148. */
  149. containsPosition( position ) {
  150. return position.isAfter( this.start ) && position.isBefore( this.end );
  151. }
  152. /**
  153. * Checks whether this range contains given {@link module:engine/view/range~Range range}.
  154. *
  155. * @param {module:engine/view/range~Range} otherRange Range to check.
  156. * @param {Boolean} [loose=false] Whether the check is loose or strict. If the check is strict (`false`), compared range cannot
  157. * start or end at the same position as this range boundaries. If the check is loose (`true`), compared range can start, end or
  158. * even be equal to this range. Note that collapsed ranges are always compared in strict mode.
  159. * @returns {Boolean} `true` if given {@link module:engine/view/range~Range range} boundaries are contained by this range, `false`
  160. * otherwise.
  161. */
  162. containsRange( otherRange, loose = false ) {
  163. if ( otherRange.isCollapsed ) {
  164. loose = false;
  165. }
  166. const containsStart = this.containsPosition( otherRange.start ) || ( loose && this.start.isEqual( otherRange.start ) );
  167. const containsEnd = this.containsPosition( otherRange.end ) || ( loose && this.end.isEqual( otherRange.end ) );
  168. return containsStart && containsEnd;
  169. }
  170. /**
  171. * Computes which part(s) of this {@link module:engine/view/range~Range range} is not a part of given
  172. * {@link module:engine/view/range~Range range}.
  173. * Returned array contains zero, one or two {@link module:engine/view/range~Range ranges}.
  174. *
  175. * Examples:
  176. *
  177. * let foo = new Text( 'foo' );
  178. * let img = new ContainerElement( 'img' );
  179. * let bar = new Text( 'bar' );
  180. * let p = new ContainerElement( 'p', null, [ foo, img, bar ] );
  181. *
  182. * let range = new Range( new Position( foo, 2 ), new Position( bar, 1 ); // "o", img, "b" are in range.
  183. * let otherRange = new Range( new Position( foo, 1 ), new Position( bar, 2 ); "oo", img, "ba" are in range.
  184. * let transformed = range.getDifference( otherRange );
  185. * // transformed array has no ranges because `otherRange` contains `range`
  186. *
  187. * otherRange = new Range( new Position( foo, 1 ), new Position( p, 2 ); // "oo", img are in range.
  188. * transformed = range.getDifference( otherRange );
  189. * // transformed array has one range: from ( p, 2 ) to ( bar, 1 )
  190. *
  191. * otherRange = new Range( new Position( p, 1 ), new Position( p, 2 ) ); // img is in range.
  192. * transformed = range.getDifference( otherRange );
  193. * // transformed array has two ranges: from ( foo, 1 ) to ( p, 1 ) and from ( p, 2 ) to ( bar, 1 )
  194. *
  195. * @param {module:engine/view/range~Range} otherRange Range to differentiate against.
  196. * @returns {Array.<module:engine/view/range~Range>} The difference between ranges.
  197. */
  198. getDifference( otherRange ) {
  199. const ranges = [];
  200. if ( this.isIntersecting( otherRange ) ) {
  201. // Ranges intersect.
  202. if ( this.containsPosition( otherRange.start ) ) {
  203. // Given range start is inside this range. This means that we have to
  204. // add shrunken range - from the start to the middle of this range.
  205. ranges.push( new Range( this.start, otherRange.start ) );
  206. }
  207. if ( this.containsPosition( otherRange.end ) ) {
  208. // Given range end is inside this range. This means that we have to
  209. // add shrunken range - from the middle of this range to the end.
  210. ranges.push( new Range( otherRange.end, this.end ) );
  211. }
  212. } else {
  213. // Ranges do not intersect, return the original range.
  214. ranges.push( Range.createFromRange( this ) );
  215. }
  216. return ranges;
  217. }
  218. /**
  219. * Returns an intersection of this {@link module:engine/view/range~Range range} and given {@link module:engine/view/range~Range range}.
  220. * Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`.
  221. *
  222. * Examples:
  223. *
  224. * let foo = new Text( 'foo' );
  225. * let img = new ContainerElement( 'img' );
  226. * let bar = new Text( 'bar' );
  227. * let p = new ContainerElement( 'p', null, [ foo, img, bar ] );
  228. *
  229. * let range = new Range( new Position( foo, 2 ), new Position( bar, 1 ); // "o", img, "b" are in range.
  230. * let otherRange = new Range( new Position( foo, 1 ), new Position( p, 2 ); // "oo", img are in range.
  231. * let transformed = range.getIntersection( otherRange ); // range from ( foo, 1 ) to ( p, 2 ).
  232. *
  233. * otherRange = new Range( new Position( bar, 1 ), new Position( bar, 3 ); "ar" is in range.
  234. * transformed = range.getIntersection( otherRange ); // null - no common part.
  235. *
  236. * @param {module:engine/view/range~Range} otherRange Range to check for intersection.
  237. * @returns {module:engine/view/range~Range|null} A common part of given ranges or `null` if ranges have no common part.
  238. */
  239. getIntersection( otherRange ) {
  240. if ( this.isIntersecting( otherRange ) ) {
  241. // Ranges intersect, so a common range will be returned.
  242. // At most, it will be same as this range.
  243. let commonRangeStart = this.start;
  244. let commonRangeEnd = this.end;
  245. if ( this.containsPosition( otherRange.start ) ) {
  246. // Given range start is inside this range. This means thaNt we have to
  247. // shrink common range to the given range start.
  248. commonRangeStart = otherRange.start;
  249. }
  250. if ( this.containsPosition( otherRange.end ) ) {
  251. // Given range end is inside this range. This means that we have to
  252. // shrink common range to the given range end.
  253. commonRangeEnd = otherRange.end;
  254. }
  255. return new Range( commonRangeStart, commonRangeEnd );
  256. }
  257. // Ranges do not intersect, so they do not have common part.
  258. return null;
  259. }
  260. /**
  261. * Creates a {@link module:engine/view/treewalker~TreeWalker TreeWalker} instance with this range as a boundary.
  262. *
  263. * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
  264. * @param {module:engine/view/position~Position} [options.startPosition]
  265. * @param {Boolean} [options.singleCharacters=false]
  266. * @param {Boolean} [options.shallow=false]
  267. * @param {Boolean} [options.ignoreElementEnd=false]
  268. */
  269. getWalker( options = {} ) {
  270. options.boundaries = this;
  271. return new TreeWalker( options );
  272. }
  273. /**
  274. * Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment}
  275. * which is a common ancestor for both positions.
  276. *
  277. * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null}
  278. */
  279. getCommonAncestor() {
  280. return this.start.getCommonAncestor( this.end );
  281. }
  282. /**
  283. * Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
  284. * them.
  285. *
  286. * This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option
  287. * set to `true`. However it returns only {@link module:engine/view/item~Item items},
  288. * 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/item~Item>}
  295. */
  296. * getItems( options = {} ) {
  297. options.boundaries = this;
  298. options.ignoreElementEnd = true;
  299. const treeWalker = new TreeWalker( options );
  300. for ( const value of treeWalker ) {
  301. yield value.item;
  302. }
  303. }
  304. /**
  305. * Returns an iterator that iterates over all {@link module:engine/view/position~Position positions} that are boundaries or
  306. * contained in this range.
  307. *
  308. * This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range. However it returns only
  309. * {@link module:engine/view/position~Position positions}, not {@link module:engine/view/treewalker~TreeWalkerValue}.
  310. *
  311. * You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for
  312. * a full list of available options.
  313. *
  314. * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
  315. * @returns {Iterable.<module:engine/view/position~Position>}
  316. */
  317. * getPositions( options = {} ) {
  318. options.boundaries = this;
  319. const treeWalker = new TreeWalker( options );
  320. yield treeWalker.position;
  321. for ( const value of treeWalker ) {
  322. yield value.nextPosition;
  323. }
  324. }
  325. /**
  326. * Checks and returns whether this range intersects with given range.
  327. *
  328. * @param {module:engine/view/range~Range} otherRange Range to compare with.
  329. * @returns {Boolean} True if ranges intersect.
  330. */
  331. isIntersecting( otherRange ) {
  332. return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start );
  333. }
  334. /**
  335. * Creates a range from given parents and offsets.
  336. *
  337. * @param {module:engine/view/element~Element} startElement Start position parent element.
  338. * @param {Number} startOffset Start position offset.
  339. * @param {module:engine/view/element~Element} endElement End position parent element.
  340. * @param {Number} endOffset End position offset.
  341. * @returns {module:engine/view/range~Range} Created range.
  342. */
  343. static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
  344. return new this(
  345. new Position( startElement, startOffset ),
  346. new Position( endElement, endOffset )
  347. );
  348. }
  349. /**
  350. * Creates and returns a new instance of Range which is equal to passed range.
  351. *
  352. * @param {module:engine/view/range~Range} range Range to clone.
  353. * @returns {module:engine/view/range~Range}
  354. */
  355. static createFromRange( range ) {
  356. return new this( range.start, range.end );
  357. }
  358. /**
  359. * Creates a new range, spreading from specified {@link module:engine/view/position~Position position} to a position moved by
  360. * given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range.
  361. *
  362. * @param {module:engine/view/position~Position} position Beginning of the range.
  363. * @param {Number} shift How long the range should be.
  364. * @returns {module:engine/view/range~Range}
  365. */
  366. static createFromPositionAndShift( position, shift ) {
  367. const start = position;
  368. const end = position.getShiftedBy( shift );
  369. return shift > 0 ? new this( start, end ) : new this( end, start );
  370. }
  371. /**
  372. * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
  373. * that element and ends after the last child of that element.
  374. *
  375. * @param {module:engine/view/element~Element} element Element which is a parent for the range.
  376. * @returns {module:engine/view/range~Range}
  377. */
  378. static createIn( element ) {
  379. return this.createFromParentsAndOffsets( element, 0, element, element.childCount );
  380. }
  381. /**
  382. * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
  383. *
  384. * @param {module:engine/view/item~Item} item
  385. * @returns {module:engine/view/range~Range}
  386. */
  387. static createOn( item ) {
  388. return this.createFromPositionAndShift( Position.createBefore( item ), 1 );
  389. }
  390. }
  391. // Function used by getEnlagred and getShrinked methods.
  392. function enlargeShrinkSkip( value ) {
  393. if ( value.item.is( 'attributeElement' ) || value.item.is( 'uiElement' ) ) {
  394. return true;
  395. }
  396. return false;
  397. }