range.js 16 KB

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