range.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/range
  7. */
  8. import Position from './position';
  9. import TreeWalker from './treewalker';
  10. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  11. /**
  12. * Range class. Range is iterable.
  13. */
  14. export default class Range {
  15. /**
  16. * Creates a range spanning from `start` position to `end` position.
  17. *
  18. * **Note:** Constructor creates it's own {@link module:engine/model/position~Position Position} instances basing on passed values.
  19. *
  20. * @param {module:engine/model/position~Position} start Start position.
  21. * @param {module:engine/model/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
  22. */
  23. constructor( start, end = null ) {
  24. /**
  25. * Start position.
  26. *
  27. * @readonly
  28. * @member {module:engine/model/position~Position}
  29. */
  30. this.start = Position.createFromPosition( start );
  31. /**
  32. * End position.
  33. *
  34. * @readonly
  35. * @member {module:engine/model/position~Position}
  36. */
  37. this.end = end ? Position.createFromPosition( end ) : Position.createFromPosition( start );
  38. }
  39. /**
  40. * Iterable interface.
  41. *
  42. * Iterates over all {@link module:engine/model/item~Item items} that are in this range and returns
  43. * them together with additional information like length or {@link module:engine/model/position~Position positions},
  44. * grouped as {@link module:engine/model/treewalker~TreeWalkerValue}.
  45. * It iterates over all {@link module:engine/model/textproxy~TextProxy text contents} that are inside the range
  46. * and all the {@link module:engine/model/element~Element}s that are entered into when iterating over this range.
  47. *
  48. * This iterator uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range
  49. * and `ignoreElementEnd` option set to `true`.
  50. *
  51. * @returns {Iterable.<module:engine/model/treewalker~TreeWalkerValue>}
  52. */
  53. * [ Symbol.iterator ]() {
  54. yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );
  55. }
  56. /**
  57. * Returns whether the range is collapsed, that is if {@link #start} and
  58. * {@link #end} positions are equal.
  59. *
  60. * @type {Boolean}
  61. */
  62. get isCollapsed() {
  63. return this.start.isEqual( this.end );
  64. }
  65. /**
  66. * Returns whether this range is flat, that is if {@link #start} position and
  67. * {@link #end} position are in the same {@link module:engine/model/position~Position#parent}.
  68. *
  69. * @type {Boolean}
  70. */
  71. get isFlat() {
  72. return this.start.parent === this.end.parent;
  73. }
  74. /**
  75. * Range root element.
  76. *
  77. * @type {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
  78. */
  79. get root() {
  80. return this.start.root;
  81. }
  82. /**
  83. * Checks whether this range contains given {@link module:engine/model/position~Position position}.
  84. *
  85. * @param {module:engine/model/position~Position} position Position to check.
  86. * @returns {Boolean} `true` if given {@link module:engine/model/position~Position position} is contained
  87. * in this range,`false` otherwise.
  88. */
  89. containsPosition( position ) {
  90. return position.isAfter( this.start ) && position.isBefore( this.end );
  91. }
  92. /**
  93. * Checks whether this range contains given {@link ~Range range}.
  94. *
  95. * @param {module:engine/model/range~Range} otherRange Range to check.
  96. * @param {Boolean} [loose=false] Whether the check is loose or strict. If the check is strict (`false`), compared range cannot
  97. * start or end at the same position as this range boundaries. If the check is loose (`true`), compared range can start, end or
  98. * even be equal to this range. Note that collapsed ranges are always compared in strict mode.
  99. * @returns {Boolean} `true` if given {@link ~Range range} boundaries are contained by this range, `false` otherwise.
  100. */
  101. containsRange( otherRange, loose = false ) {
  102. if ( otherRange.isCollapsed ) {
  103. loose = false;
  104. }
  105. const containsStart = this.containsPosition( otherRange.start ) || ( loose && this.start.isEqual( otherRange.start ) );
  106. const containsEnd = this.containsPosition( otherRange.end ) || ( loose && this.end.isEqual( otherRange.end ) );
  107. return containsStart && containsEnd;
  108. }
  109. /**
  110. * Checks whether given {@link module:engine/model/item~Item} is inside this range.
  111. *
  112. * @param {module:engine/model/item~Item} item Model item to check.
  113. */
  114. containsItem( item ) {
  115. const pos = Position.createBefore( item );
  116. return this.containsPosition( pos ) || this.start.isEqual( pos );
  117. }
  118. /**
  119. * Two ranges are equal if their {@link #start} and {@link #end} positions are equal.
  120. *
  121. * @param {module:engine/model/range~Range} otherRange Range to compare with.
  122. * @returns {Boolean} `true` if ranges are equal, `false` otherwise.
  123. */
  124. isEqual( otherRange ) {
  125. return this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end );
  126. }
  127. /**
  128. * Checks and returns whether this range intersects with given range.
  129. *
  130. * @param {module:engine/model/range~Range} otherRange Range to compare with.
  131. * @returns {Boolean} `true` if ranges intersect, `false` otherwise.
  132. */
  133. isIntersecting( otherRange ) {
  134. return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start );
  135. }
  136. /**
  137. * Computes which part(s) of this {@link ~Range range} is not a part of given {@link ~Range range}.
  138. * Returned array contains zero, one or two {@link ~Range ranges}.
  139. *
  140. * Examples:
  141. *
  142. * let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) );
  143. * let otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 5 ] ) );
  144. * let transformed = range.getDifference( otherRange );
  145. * // transformed array has no ranges because `otherRange` contains `range`
  146. *
  147. * otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) );
  148. * transformed = range.getDifference( otherRange );
  149. * // transformed array has one range: from [ 3 ] to [ 4, 0, 1 ]
  150. *
  151. * otherRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 4 ] ) );
  152. * transformed = range.getDifference( otherRange );
  153. * // transformed array has two ranges: from [ 2, 7 ] to [ 3 ] and from [ 4 ] to [ 4, 0, 1 ]
  154. *
  155. * @param {module:engine/model/range~Range} otherRange Range to differentiate against.
  156. * @returns {Array.<module:engine/model/range~Range>} The difference between ranges.
  157. */
  158. getDifference( otherRange ) {
  159. const ranges = [];
  160. if ( this.isIntersecting( otherRange ) ) {
  161. // Ranges intersect.
  162. if ( this.containsPosition( otherRange.start ) ) {
  163. // Given range start is inside this range. This means that we have to
  164. // add shrunken range - from the start to the middle of this range.
  165. ranges.push( new Range( this.start, otherRange.start ) );
  166. }
  167. if ( this.containsPosition( otherRange.end ) ) {
  168. // Given range end is inside this range. This means that we have to
  169. // add shrunken range - from the middle of this range to the end.
  170. ranges.push( new Range( otherRange.end, this.end ) );
  171. }
  172. } else {
  173. // Ranges do not intersect, return the original range.
  174. ranges.push( Range.createFromRange( this ) );
  175. }
  176. return ranges;
  177. }
  178. /**
  179. * Returns an intersection of this {@link ~Range range} and given {@link ~Range range}.
  180. * Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`.
  181. *
  182. * Examples:
  183. *
  184. * let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) );
  185. * let otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
  186. * let transformed = range.getIntersection( otherRange ); // null - ranges have no common part
  187. *
  188. * otherRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 5 ] ) );
  189. * transformed = range.getIntersection( otherRange ); // range from [ 3 ] to [ 4, 0, 1 ]
  190. *
  191. * @param {module:engine/model/range~Range} otherRange Range to check for intersection.
  192. * @returns {module:engine/model/range~Range|null} A common part of given ranges or `null` if ranges have no common part.
  193. */
  194. getIntersection( otherRange ) {
  195. if ( this.isIntersecting( otherRange ) ) {
  196. // Ranges intersect, so a common range will be returned.
  197. // At most, it will be same as this range.
  198. let commonRangeStart = this.start;
  199. let commonRangeEnd = this.end;
  200. if ( this.containsPosition( otherRange.start ) ) {
  201. // Given range start is inside this range. This means thaNt we have to
  202. // shrink common range to the given range start.
  203. commonRangeStart = otherRange.start;
  204. }
  205. if ( this.containsPosition( otherRange.end ) ) {
  206. // Given range end is inside this range. This means that we have to
  207. // shrink common range to the given range end.
  208. commonRangeEnd = otherRange.end;
  209. }
  210. return new Range( commonRangeStart, commonRangeEnd );
  211. }
  212. // Ranges do not intersect, so they do not have common part.
  213. return null;
  214. }
  215. /**
  216. * Computes and returns the smallest set of {@link #isFlat flat} ranges, that covers this range in whole.
  217. *
  218. * See an example of a model structure (`[` and `]` are range boundaries):
  219. *
  220. * root root
  221. * |- element DIV DIV P2 P3 DIV
  222. * | |- element H H P1 f o o b a r H P4
  223. * | | |- "fir[st" fir[st lorem se]cond ipsum
  224. * | |- element P1
  225. * | | |- "lorem" ||
  226. * |- element P2 ||
  227. * | |- "foo" VV
  228. * |- element P3
  229. * | |- "bar" root
  230. * |- element DIV DIV [P2 P3] DIV
  231. * | |- element H H [P1] f o o b a r H P4
  232. * | | |- "se]cond" fir[st] lorem [se]cond ipsum
  233. * | |- element P4
  234. * | | |- "ipsum"
  235. *
  236. * As it can be seen, letters contained in the range are: `stloremfoobarse`, spread across different parents.
  237. * We are looking for minimal set of flat ranges that contains the same nodes.
  238. *
  239. * Minimal flat ranges for above range `( [ 0, 0, 3 ], [ 3, 0, 2 ] )` will be:
  240. *
  241. * ( [ 0, 0, 3 ], [ 0, 0, 5 ] ) = "st"
  242. * ( [ 0, 1 ], [ 0, 2 ] ) = element P1 ("lorem")
  243. * ( [ 1 ], [ 3 ] ) = element P2, element P3 ("foobar")
  244. * ( [ 3, 0, 0 ], [ 3, 0, 2 ] ) = "se"
  245. *
  246. * **Note:** if an {@link module:engine/model/element~Element element} is not wholly contained in this range, it won't be returned
  247. * in any of the returned flat ranges. See in the example how `H` elements at the beginning and at the end of the range
  248. * were omitted. Only their parts that were wholly in the range were returned.
  249. *
  250. * **Note:** this method is not returning flat ranges that contain no nodes.
  251. *
  252. * @returns {Array.<module:engine/model/range~Range>} Array of flat ranges covering this range.
  253. */
  254. getMinimalFlatRanges() {
  255. const ranges = [];
  256. const diffAt = this.start.getCommonPath( this.end ).length;
  257. const pos = Position.createFromPosition( this.start );
  258. let posParent = pos.parent;
  259. // Go up.
  260. while ( pos.path.length > diffAt + 1 ) {
  261. const howMany = posParent.maxOffset - pos.offset;
  262. if ( howMany !== 0 ) {
  263. ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
  264. }
  265. pos.path = pos.path.slice( 0, -1 );
  266. pos.offset++;
  267. posParent = posParent.parent;
  268. }
  269. // Go down.
  270. while ( pos.path.length <= this.end.path.length ) {
  271. const offset = this.end.path[ pos.path.length - 1 ];
  272. const howMany = offset - pos.offset;
  273. if ( howMany !== 0 ) {
  274. ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
  275. }
  276. pos.offset = offset;
  277. pos.path.push( 0 );
  278. }
  279. return ranges;
  280. }
  281. /**
  282. * Creates a {@link module:engine/model/treewalker~TreeWalker TreeWalker} instance with this range as a boundary.
  283. *
  284. * @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
  285. * @param {module:engine/model/position~Position} [options.startPosition]
  286. * @param {Boolean} [options.singleCharacters=false]
  287. * @param {Boolean} [options.shallow=false]
  288. * @param {Boolean} [options.ignoreElementEnd=false]
  289. */
  290. getWalker( options = {} ) {
  291. options.boundaries = this;
  292. return new TreeWalker( options );
  293. }
  294. /**
  295. * Returns an iterator that iterates over all {@link module:engine/model/item~Item items} that are in this range and returns
  296. * them.
  297. *
  298. * This method uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option
  299. * set to `true`. However it returns only {@link module:engine/model/item~Item model items},
  300. * not {@link module:engine/model/treewalker~TreeWalkerValue}.
  301. *
  302. * You may specify additional options for the tree walker. See {@link module:engine/model/treewalker~TreeWalker} for
  303. * a full list of available options.
  304. *
  305. * @method getItems
  306. * @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
  307. * @returns {Iterable.<module:engine/model/item~Item>}
  308. */
  309. * getItems( options = {} ) {
  310. options.boundaries = this;
  311. options.ignoreElementEnd = true;
  312. const treeWalker = new TreeWalker( options );
  313. for ( const value of treeWalker ) {
  314. yield value.item;
  315. }
  316. }
  317. /**
  318. * Returns an iterator that iterates over all {@link module:engine/model/position~Position positions} that are boundaries or
  319. * contained in this range.
  320. *
  321. * This method uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range. However it returns only
  322. * {@link module:engine/model/position~Position positions}, not {@link module:engine/model/treewalker~TreeWalkerValue}.
  323. *
  324. * You may specify additional options for the tree walker. See {@link module:engine/model/treewalker~TreeWalker} for
  325. * a full list of available options.
  326. *
  327. * @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
  328. * @returns {Iterable.<module:engine/model/position~Position>}
  329. */
  330. * getPositions( options = {} ) {
  331. options.boundaries = this;
  332. const treeWalker = new TreeWalker( options );
  333. yield treeWalker.position;
  334. for ( const value of treeWalker ) {
  335. yield value.nextPosition;
  336. }
  337. }
  338. /**
  339. * Returns a range that is a result of transforming this range by given `delta`.
  340. *
  341. * **Note:** transformation may break one range into multiple ranges (e.g. when a part of the range is
  342. * moved to a different part of document tree). For this reason, an array is returned by this method and it
  343. * may contain one or more `Range` instances.
  344. *
  345. * @param {module:engine/model/delta/delta~Delta} delta Delta to transform range by.
  346. * @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation.
  347. */
  348. getTransformedByDelta( delta ) {
  349. const ranges = [ Range.createFromRange( this ) ];
  350. // Operation types that a range can be transformed by.
  351. const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] );
  352. for ( const operation of delta.operations ) {
  353. if ( supportedTypes.has( operation.type ) ) {
  354. for ( let i = 0; i < ranges.length; i++ ) {
  355. let result;
  356. if ( operation.type == 'insert' ) {
  357. result = ranges[ i ]._getTransformedByDocumentChange(
  358. operation.type,
  359. delta.type,
  360. operation.position,
  361. operation.nodes.maxOffset
  362. );
  363. } else {
  364. result = ranges[ i ]._getTransformedByDocumentChange(
  365. operation.type,
  366. delta.type,
  367. operation.targetPosition,
  368. operation.howMany,
  369. operation.sourcePosition
  370. );
  371. }
  372. ranges.splice( i, 1, ...result );
  373. i += result.length - 1;
  374. }
  375. }
  376. }
  377. return ranges;
  378. }
  379. /**
  380. * Returns a range that is a result of transforming this range by multiple `deltas`.
  381. *
  382. * **Note:** transformation may break one range into multiple ranges (e.g. when a part of the range is
  383. * moved to a different part of document tree). For this reason, an array is returned by this method and it
  384. * may contain one or more `Range` instances.
  385. *
  386. * @param {Iterable.<module:engine/model/delta/delta~Delta>} deltas Deltas to transform the range by.
  387. * @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation.
  388. */
  389. getTransformedByDeltas( deltas ) {
  390. const ranges = [ Range.createFromRange( this ) ];
  391. for ( const delta of deltas ) {
  392. for ( let i = 0; i < ranges.length; i++ ) {
  393. const result = ranges[ i ].getTransformedByDelta( delta );
  394. ranges.splice( i, 1, ...result );
  395. i += result.length - 1;
  396. }
  397. }
  398. // It may happen that a range is split into two, and then the part of second "piece" is moved into first
  399. // "piece". In this case we will have incorrect third range, which should not be included in the result --
  400. // because it is already included in the first "piece". In this loop we are looking for all such ranges that
  401. // are inside other ranges and we simply remove them.
  402. for ( let i = 0; i < ranges.length; i++ ) {
  403. const range = ranges[ i ];
  404. for ( let j = i + 1; j < ranges.length; j++ ) {
  405. const next = ranges[ j ];
  406. if ( range.containsRange( next ) || next.containsRange( range ) || range.isEqual( next ) ) {
  407. ranges.splice( j, 1 );
  408. }
  409. }
  410. }
  411. return ranges;
  412. }
  413. /**
  414. * Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
  415. * which is a common ancestor of the range's both ends (in which the entire range is contained).
  416. *
  417. * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
  418. */
  419. getCommonAncestor() {
  420. return this.start.getCommonAncestor( this.end );
  421. }
  422. /**
  423. * Returns a range that is a result of transforming this range by a change in the model document.
  424. *
  425. * @protected
  426. * @param {'insert'|'move'|'remove'|'reinsert'} type Change type.
  427. * @param {String} deltaType Type of delta that introduced the change.
  428. * @param {module:engine/model/position~Position} targetPosition Position before the first changed node.
  429. * @param {Number} howMany How many nodes has been changed.
  430. * @param {module:engine/model/position~Position} sourcePosition Source position of changes.
  431. * @returns {Array.<module:engine/model/range~Range>}
  432. */
  433. _getTransformedByDocumentChange( type, deltaType, targetPosition, howMany, sourcePosition ) {
  434. if ( type == 'insert' ) {
  435. return this._getTransformedByInsertion( targetPosition, howMany, false, false );
  436. } else {
  437. const sourceRange = Range.createFromPositionAndShift( sourcePosition, howMany );
  438. // Edge case for merge delta.
  439. if (
  440. deltaType == 'merge' &&
  441. this.isCollapsed &&
  442. ( this.start.isEqual( sourceRange.start ) || this.start.isEqual( sourceRange.end ) )
  443. ) {
  444. // Collapsed range is in merged element, at the beginning or at the end of it.
  445. // Without fix, the range would end up in the graveyard, together with removed element.
  446. // <p>foo</p><p>[]bar</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foo[]bar</p>
  447. // <p>foo</p><p>bar[]</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foobar[]</p>
  448. //
  449. // In most cases, `sourceRange.start.offset` for merge delta's move operation would be 0,
  450. // so this formula might look overcomplicated.
  451. // However in some scenarios, after operational transformation, move operation might not
  452. // in fact start from 0 and we need to properly count new offset.
  453. // https://github.com/ckeditor/ckeditor5-engine/pull/1133#issuecomment-329080668.
  454. const offset = this.start.offset - sourceRange.start.offset;
  455. return [ new Range( targetPosition.getShiftedBy( offset ) ) ];
  456. }
  457. //
  458. // Edge case for split delta.
  459. //
  460. if ( deltaType == 'split' && this.isCollapsed && this.end.isEqual( sourceRange.end ) ) {
  461. // Collapsed range is at the end of split element.
  462. // Without fix, the range would end up at the end of split (old) element instead of at the end of new element.
  463. // That would happen because this range is not technically inside moved range. Last step below shows the fix.
  464. // <p>foobar[]</p> -> <p>foobar[]</p><p></p> -> <p>foo[]</p><p>bar</p> -> <p>foo</p><p>bar[]</p>
  465. return [ new Range( targetPosition.getShiftedBy( howMany ) ) ];
  466. }
  467. //
  468. // Other edge cases:
  469. //
  470. // In all examples `[]` is `this` and `{}` is `sourceRange`, while `^` is move target position.
  471. //
  472. // Example:
  473. // <p>xx</p>^<w>{<p>a[b</p>}</w><p>c]d</p> --> <p>xx</p><p>a[b</p><w></w><p>c]d</p>
  474. // ^<p>xx</p><w>{<p>a[b</p>}</w><p>c]d</p> --> <p>a[b</p><p>xx</p><w></w><p>c]d</p> // Note <p>xx</p> inclusion.
  475. // <w>{<p>a[b</p>}</w>^<p>c]d</p> --> <w></w><p>a[b</p><p>c]d</p>
  476. if (
  477. ( sourceRange.containsPosition( this.start ) || sourceRange.start.isEqual( this.start ) ) &&
  478. this.containsPosition( sourceRange.end ) &&
  479. this.end.isAfter( targetPosition )
  480. ) {
  481. const start = this.start._getCombined(
  482. sourcePosition,
  483. targetPosition._getTransformedByDeletion( sourcePosition, howMany )
  484. );
  485. const end = this.end._getTransformedByMove( sourcePosition, targetPosition, howMany, false, false );
  486. return [ new Range( start, end ) ];
  487. }
  488. // Example:
  489. // <p>c[d</p><w>{<p>a]b</p>}</w>^<p>xx</p> --> <p>c[d</p><w></w><p>a]b</p><p>xx</p>
  490. // <p>c[d</p><w>{<p>a]b</p>}</w><p>xx</p>^ --> <p>c[d</p><w></w><p>xx</p><p>a]b</p> // Note <p>xx</p> inclusion.
  491. // <p>c[d</p>^<w>{<p>a]b</p>}</w> --> <p>c[d</p><p>a]b</p><w></w>
  492. if (
  493. ( sourceRange.containsPosition( this.end ) || sourceRange.end.isEqual( this.end ) ) &&
  494. this.containsPosition( sourceRange.start ) &&
  495. this.start.isBefore( targetPosition )
  496. ) {
  497. const start = this.start._getTransformedByMove(
  498. sourcePosition,
  499. targetPosition,
  500. howMany,
  501. true,
  502. false
  503. );
  504. const end = this.end._getCombined(
  505. sourcePosition,
  506. targetPosition._getTransformedByDeletion( sourcePosition, howMany )
  507. );
  508. return [ new Range( start, end ) ];
  509. }
  510. return this._getTransformedByMove( sourcePosition, targetPosition, howMany );
  511. }
  512. }
  513. /**
  514. * Returns an array containing one or two {@link ~Range ranges} that are a result of transforming this
  515. * {@link ~Range range} by inserting `howMany` nodes at `insertPosition`. Two {@link ~Range ranges} are
  516. * returned if the insertion was inside this {@link ~Range range} and `spread` is set to `true`.
  517. *
  518. * Examples:
  519. *
  520. * let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) );
  521. * let transformed = range._getTransformedByInsertion( new Position( root, [ 1 ] ), 2 );
  522. * // transformed array has one range from [ 4, 7 ] to [ 6, 0, 1 ]
  523. *
  524. * transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 0 ] ), 4 );
  525. * // transformed array has one range from [ 2, 7 ] to [ 4, 0, 5 ]
  526. *
  527. * transformed = range._getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4 );
  528. * // transformed array has one range, which is equal to original range
  529. *
  530. * transformed = range._getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4, true );
  531. * // transformed array has two ranges: from [ 2, 7 ] to [ 3, 2 ] and from [ 3, 6 ] to [ 4, 0, 1 ]
  532. *
  533. * transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, false );
  534. * // transformed array has one range which is equal to original range because insertion is after the range boundary
  535. *
  536. * transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, true );
  537. * // transformed array has one range: from [ 2, 7 ] to [ 4, 0, 5 ] because range was expanded
  538. *
  539. * @protected
  540. * @param {module:engine/model/position~Position} insertPosition Position where nodes are inserted.
  541. * @param {Number} howMany How many nodes are inserted.
  542. * @param {Boolean} [spread] Flag indicating whether this {~Range range} should be spread if insertion
  543. * was inside the range. Defaults to `false`.
  544. * @param {Boolean} [isSticky] Flag indicating whether insertion should expand a range if it is in a place of
  545. * range boundary. Defaults to `false`.
  546. * @returns {Array.<module:engine/model/range~Range>} Result of the transformation.
  547. */
  548. _getTransformedByInsertion( insertPosition, howMany, spread = false, isSticky = false ) {
  549. if ( spread && this.containsPosition( insertPosition ) ) {
  550. // Range has to be spread. The first part is from original start to the spread point.
  551. // The other part is from spread point to the original end, but transformed by
  552. // insertion to reflect insertion changes.
  553. return [
  554. new Range( this.start, insertPosition ),
  555. new Range(
  556. insertPosition._getTransformedByInsertion( insertPosition, howMany, true ),
  557. this.end._getTransformedByInsertion( insertPosition, howMany, this.isCollapsed )
  558. )
  559. ];
  560. } else {
  561. const range = Range.createFromRange( this );
  562. const insertBeforeStart = !isSticky;
  563. const insertBeforeEnd = range.isCollapsed ? true : isSticky;
  564. range.start = range.start._getTransformedByInsertion( insertPosition, howMany, insertBeforeStart );
  565. range.end = range.end._getTransformedByInsertion( insertPosition, howMany, insertBeforeEnd );
  566. return [ range ];
  567. }
  568. }
  569. /**
  570. * Returns an array containing {@link ~Range ranges} that are a result of transforming this
  571. * {@link ~Range range} by moving `howMany` nodes from `sourcePosition` to `targetPosition`.
  572. *
  573. * @protected
  574. * @param {module:engine/model/position~Position} sourcePosition Position from which nodes are moved.
  575. * @param {module:engine/model/position~Position} targetPosition Position to where nodes are moved.
  576. * @param {Number} howMany How many nodes are moved.
  577. * @returns {Array.<module:engine/model/range~Range>} Result of the transformation.
  578. */
  579. _getTransformedByMove( sourcePosition, targetPosition, howMany ) {
  580. if ( this.isCollapsed ) {
  581. const newPos = this.start._getTransformedByMove( sourcePosition, targetPosition, howMany, true, false );
  582. return [ new Range( newPos ) ];
  583. }
  584. let result;
  585. const moveRange = new Range( sourcePosition, sourcePosition.getShiftedBy( howMany ) );
  586. const differenceSet = this.getDifference( moveRange );
  587. let difference = null;
  588. const common = this.getIntersection( moveRange );
  589. if ( differenceSet.length == 1 ) {
  590. // `moveRange` and this range may intersect.
  591. difference = new Range(
  592. differenceSet[ 0 ].start._getTransformedByDeletion( sourcePosition, howMany ),
  593. differenceSet[ 0 ].end._getTransformedByDeletion( sourcePosition, howMany )
  594. );
  595. } else if ( differenceSet.length == 2 ) {
  596. // `moveRange` is inside this range.
  597. difference = new Range(
  598. this.start,
  599. this.end._getTransformedByDeletion( sourcePosition, howMany )
  600. );
  601. } // else, `moveRange` contains this range.
  602. const insertPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
  603. if ( difference ) {
  604. result = difference._getTransformedByInsertion( insertPosition, howMany, common !== null );
  605. } else {
  606. result = [];
  607. }
  608. if ( common ) {
  609. result.push( new Range(
  610. common.start._getCombined( moveRange.start, insertPosition ),
  611. common.end._getCombined( moveRange.start, insertPosition )
  612. ) );
  613. }
  614. return result;
  615. }
  616. /**
  617. * Creates a new range, spreading from specified {@link module:engine/model/position~Position position} to a position moved by
  618. * given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range.
  619. *
  620. * @param {module:engine/model/position~Position} position Beginning of the range.
  621. * @param {Number} shift How long the range should be.
  622. * @returns {module:engine/model/range~Range}
  623. */
  624. static createFromPositionAndShift( position, shift ) {
  625. const start = position;
  626. const end = position.getShiftedBy( shift );
  627. return shift > 0 ? new this( start, end ) : new this( end, start );
  628. }
  629. /**
  630. * Creates a range from given parents and offsets.
  631. *
  632. * @param {module:engine/model/element~Element} startElement Start position parent element.
  633. * @param {Number} startOffset Start position offset.
  634. * @param {module:engine/model/element~Element} endElement End position parent element.
  635. * @param {Number} endOffset End position offset.
  636. * @returns {module:engine/model/range~Range}
  637. */
  638. static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
  639. return new this(
  640. Position.createFromParentAndOffset( startElement, startOffset ),
  641. Position.createFromParentAndOffset( endElement, endOffset )
  642. );
  643. }
  644. /**
  645. * Creates a new instance of `Range` which is equal to passed range.
  646. *
  647. * @param {module:engine/model/range~Range} range Range to clone.
  648. * @returns {module:engine/model/range~Range}
  649. */
  650. static createFromRange( range ) {
  651. return new this( range.start, range.end );
  652. }
  653. /**
  654. * Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
  655. * that element and ends after the last child of that element.
  656. *
  657. * @param {module:engine/model/element~Element} element Element which is a parent for the range.
  658. * @returns {module:engine/model/range~Range}
  659. */
  660. static createIn( element ) {
  661. return this.createFromParentsAndOffsets( element, 0, element, element.maxOffset );
  662. }
  663. /**
  664. * Creates a range that starts before given {@link module:engine/model/item~Item model item} and ends after it.
  665. *
  666. * @param {module:engine/model/item~Item} item
  667. * @returns {module:engine/model/range~Range}
  668. */
  669. static createOn( item ) {
  670. return this.createFromPositionAndShift( Position.createBefore( item ), item.offsetSize );
  671. }
  672. /**
  673. * Creates a collapsed range at given {@link module:engine/model/position~Position position}
  674. * or on the given {@link module:engine/model/item~Item item}.
  675. *
  676. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  677. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  678. * first parameter is a {@link module:engine/model/item~Item model item}.
  679. */
  680. static createCollapsedAt( itemOrPosition, offset ) {
  681. const start = Position.createAt( itemOrPosition, offset );
  682. const end = Position.createFromPosition( start );
  683. return new Range( start, end );
  684. }
  685. /**
  686. * Combines all ranges from the passed array into a one range. At least one range has to be passed.
  687. * Passed ranges must not have common parts.
  688. *
  689. * The first range from the array is a reference range. If other ranges start or end on the exactly same position where
  690. * the reference range, they get combined into one range.
  691. *
  692. * [ ][] [ ][ ][ ][ ][] [ ] // Passed ranges, shown sorted
  693. * [ ] // The result of the function if the first range was a reference range.
  694. * [ ] // The result of the function if the third-to-seventh range was a reference range.
  695. * [ ] // The result of the function if the last range was a reference range.
  696. *
  697. * @param {Array.<module:engine/model/range~Range>} ranges Ranges to combine.
  698. * @returns {module:engine/model/range~Range} Combined range.
  699. */
  700. static createFromRanges( ranges ) {
  701. if ( ranges.length === 0 ) {
  702. /**
  703. * At least one range has to be passed to
  704. * {@link module:engine/model/range~Range.createFromRanges `Range.createFromRanges()`}.
  705. *
  706. * @error range-create-from-ranges-empty-array
  707. */
  708. throw new CKEditorError( 'range-create-from-ranges-empty-array: At least one range has to be passed.' );
  709. } else if ( ranges.length == 1 ) {
  710. return this.createFromRange( ranges[ 0 ] );
  711. }
  712. // 1. Set the first range in `ranges` array as a reference range.
  713. // If we are going to return just a one range, one of the ranges need to be the reference one.
  714. // Other ranges will be stuck to that range, if possible.
  715. const ref = ranges[ 0 ];
  716. // 2. Sort all the ranges so it's easier to process them.
  717. ranges.sort( ( a, b ) => {
  718. return a.start.isAfter( b.start ) ? 1 : -1;
  719. } );
  720. // 3. Check at which index the reference range is now.
  721. const refIndex = ranges.indexOf( ref );
  722. // 4. At this moment we don't need the original range.
  723. // We are going to modify the result and we need to return a new instance of Range.
  724. // We have to create a copy of the reference range.
  725. const result = new this( ref.start, ref.end );
  726. // 5. Ranges should be checked and glued starting from the range that is closest to the reference range.
  727. // Since ranges are sorted, start with the range with index that is closest to reference range index.
  728. for ( let i = refIndex - 1; i >= 0; i++ ) {
  729. if ( ranges[ i ].end.isEqual( result.start ) ) {
  730. result.start = Position.createFromPosition( ranges[ i ].start );
  731. } else {
  732. // If ranges are not starting/ending at the same position there is no point in looking further.
  733. break;
  734. }
  735. }
  736. // 6. Ranges should be checked and glued starting from the range that is closest to the reference range.
  737. // Since ranges are sorted, start with the range with index that is closest to reference range index.
  738. for ( let i = refIndex + 1; i < ranges.length; i++ ) {
  739. if ( ranges[ i ].start.isEqual( result.end ) ) {
  740. result.end = Position.createFromPosition( ranges[ i ].end );
  741. } else {
  742. // If ranges are not starting/ending at the same position there is no point in looking further.
  743. break;
  744. }
  745. }
  746. return result;
  747. }
  748. /**
  749. * Creates a `Range` instance from given plain object (i.e. parsed JSON string).
  750. *
  751. * @param {Object} json Plain object to be converted to `Range`.
  752. * @param {module:engine/model/document~Document} doc Document object that will be range owner.
  753. * @returns {module:engine/model/element~Element} `Range` instance created using given plain object.
  754. */
  755. static fromJSON( json, doc ) {
  756. return new this( Position.fromJSON( json.start, doc ), Position.fromJSON( json.end, doc ) );
  757. }
  758. }