range.js 29 KB

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