range.js 36 KB

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