selection.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module engine/view/selection
  7. */
  8. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  9. import Range from './range';
  10. import Position from './position';
  11. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  12. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  13. import Node from './node';
  14. import count from '@ckeditor/ckeditor5-utils/src/count';
  15. import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  16. import DocumentSelection from './documentselection';
  17. /**
  18. * Class representing an arbirtary selection in the view.
  19. * See also {@link module:engine/view/documentselection~DocumentSelection}.
  20. *
  21. * New selection instances can be created via the constructor or one these methods:
  22. *
  23. * * {@link module:engine/view/view~View#createSelection `View#createSelection()`},
  24. * * {@link module:engine/view/upcastwriter~UpcastWriter#createSelection `UpcastWriter#createSelection()`}.
  25. *
  26. * A selection can consist of {@link module:engine/view/range~Range ranges} that can be set by using
  27. * the {@link module:engine/view/selection~Selection#setTo `Selection#setTo()`} method.
  28. */
  29. export default class Selection {
  30. /**
  31. * Creates new selection instance.
  32. *
  33. * **Note**: The selection constructor is available as a factory method:
  34. *
  35. * * {@link module:engine/view/view~View#createSelection `View#createSelection()`},
  36. * * {@link module:engine/view/upcastwriter~UpcastWriter#createSelection `UpcastWriter#createSelection()`}.
  37. *
  38. * // Creates empty selection without ranges.
  39. * const selection = writer.createSelection();
  40. *
  41. * // Creates selection at the given range.
  42. * const range = writer.createRange( start, end );
  43. * const selection = writer.createSelection( range );
  44. *
  45. * // Creates selection at the given ranges
  46. * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
  47. * const selection = writer.createSelection( ranges );
  48. *
  49. * // Creates selection from the other selection.
  50. * const otherSelection = writer.createSelection();
  51. * const selection = writer.createSelection( otherSelection );
  52. *
  53. * // Creates selection from the document selection.
  54. * const selection = writer.createSelection( editor.editing.view.document.selection );
  55. *
  56. * // Creates selection at the given position.
  57. * const position = writer.createPositionFromPath( root, path );
  58. * const selection = writer.createSelection( position );
  59. *
  60. * // Creates collapsed selection at the position of given item and offset.
  61. * const paragraph = writer.createContainerElement( 'paragraph' );
  62. * const selection = writer.createSelection( paragraph, offset );
  63. *
  64. * // Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
  65. * // first child of that element and ends after the last child of that element.
  66. * const selection = writer.createSelection( paragraph, 'in' );
  67. *
  68. * // Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
  69. * // just after the item.
  70. * const selection = writer.createSelection( paragraph, 'on' );
  71. *
  72. * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
  73. *
  74. * // Creates backward selection.
  75. * const selection = writer.createSelection( range, { backward: true } );
  76. *
  77. * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
  78. * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
  79. * represented in other way, for example by applying proper CSS class.
  80. *
  81. * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
  82. * (and be properly handled by screen readers).
  83. *
  84. * // Creates fake selection with label.
  85. * const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
  86. *
  87. * @param {module:engine/view/selection~Selectable} [selectable=null]
  88. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
  89. * @param {Object} [options]
  90. * @param {Boolean} [options.backward] Sets this selection instance to be backward.
  91. * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
  92. * @param {String} [options.label] Label for the fake selection.
  93. */
  94. constructor( selectable = null, placeOrOffset, options ) {
  95. /**
  96. * Stores all ranges that are selected.
  97. *
  98. * @protected
  99. * @member {Array.<module:engine/view/range~Range>}
  100. */
  101. this._ranges = [];
  102. /**
  103. * Specifies whether the last added range was added as a backward or forward range.
  104. *
  105. * @protected
  106. * @member {Boolean}
  107. */
  108. this._lastRangeBackward = false;
  109. /**
  110. * Specifies whether selection instance is fake.
  111. *
  112. * @private
  113. * @member {Boolean}
  114. */
  115. this._isFake = false;
  116. /**
  117. * Fake selection's label.
  118. *
  119. * @private
  120. * @member {String}
  121. */
  122. this._fakeSelectionLabel = '';
  123. this.setTo( selectable, placeOrOffset, options );
  124. }
  125. /**
  126. * Returns true if selection instance is marked as `fake`.
  127. *
  128. * @see #setTo
  129. * @returns {Boolean}
  130. */
  131. get isFake() {
  132. return this._isFake;
  133. }
  134. /**
  135. * Returns fake selection label.
  136. *
  137. * @see #setTo
  138. * @returns {String}
  139. */
  140. get fakeSelectionLabel() {
  141. return this._fakeSelectionLabel;
  142. }
  143. /**
  144. * Selection anchor. Anchor may be described as a position where the selection starts. Together with
  145. * {@link #focus focus} they define the direction of selection, which is important
  146. * when expanding/shrinking selection. Anchor is always the start or end of the most recent added range.
  147. * It may be a bit unintuitive when there are multiple ranges in selection.
  148. *
  149. * @see #focus
  150. * @type {module:engine/view/position~Position}
  151. */
  152. get anchor() {
  153. if ( !this._ranges.length ) {
  154. return null;
  155. }
  156. const range = this._ranges[ this._ranges.length - 1 ];
  157. const anchor = this._lastRangeBackward ? range.end : range.start;
  158. return anchor.clone();
  159. }
  160. /**
  161. * Selection focus. Focus is a position where the selection ends.
  162. *
  163. * @see #anchor
  164. * @type {module:engine/view/position~Position}
  165. */
  166. get focus() {
  167. if ( !this._ranges.length ) {
  168. return null;
  169. }
  170. const range = this._ranges[ this._ranges.length - 1 ];
  171. const focus = this._lastRangeBackward ? range.start : range.end;
  172. return focus.clone();
  173. }
  174. /**
  175. * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
  176. * collapsed.
  177. *
  178. * @type {Boolean}
  179. */
  180. get isCollapsed() {
  181. return this.rangeCount === 1 && this._ranges[ 0 ].isCollapsed;
  182. }
  183. /**
  184. * Returns number of ranges in selection.
  185. *
  186. * @type {Number}
  187. */
  188. get rangeCount() {
  189. return this._ranges.length;
  190. }
  191. /**
  192. * Specifies whether the {@link #focus} precedes {@link #anchor}.
  193. *
  194. * @type {Boolean}
  195. */
  196. get isBackward() {
  197. return !this.isCollapsed && this._lastRangeBackward;
  198. }
  199. /**
  200. * {@link module:engine/view/editableelement~EditableElement EditableElement} instance that contains this selection, or `null`
  201. * if the selection is not inside an editable element.
  202. *
  203. * @type {module:engine/view/editableelement~EditableElement|null}
  204. */
  205. get editableElement() {
  206. if ( this.anchor ) {
  207. return this.anchor.editableElement;
  208. }
  209. return null;
  210. }
  211. /**
  212. * Returns an iterable that contains copies of all ranges added to the selection.
  213. *
  214. * @returns {Iterable.<module:engine/view/range~Range>}
  215. */
  216. * getRanges() {
  217. for ( const range of this._ranges ) {
  218. yield range.clone();
  219. }
  220. }
  221. /**
  222. * Returns copy of the first range in the selection. First range is the one which
  223. * {@link module:engine/view/range~Range#start start} position {@link module:engine/view/position~Position#isBefore is before} start
  224. * position of all other ranges (not to confuse with the first range added to the selection).
  225. * Returns `null` if no ranges are added to selection.
  226. *
  227. * @returns {module:engine/view/range~Range|null}
  228. */
  229. getFirstRange() {
  230. let first = null;
  231. for ( const range of this._ranges ) {
  232. if ( !first || range.start.isBefore( first.start ) ) {
  233. first = range;
  234. }
  235. }
  236. return first ? first.clone() : null;
  237. }
  238. /**
  239. * Returns copy of the last range in the selection. Last range is the one which {@link module:engine/view/range~Range#end end}
  240. * position {@link module:engine/view/position~Position#isAfter is after} end position of all other ranges (not to confuse
  241. * with the last range added to the selection). Returns `null` if no ranges are added to selection.
  242. *
  243. * @returns {module:engine/view/range~Range|null}
  244. */
  245. getLastRange() {
  246. let last = null;
  247. for ( const range of this._ranges ) {
  248. if ( !last || range.end.isAfter( last.end ) ) {
  249. last = range;
  250. }
  251. }
  252. return last ? last.clone() : null;
  253. }
  254. /**
  255. * Returns copy of the first position in the selection. First position is the position that
  256. * {@link module:engine/view/position~Position#isBefore is before} any other position in the selection ranges.
  257. * Returns `null` if no ranges are added to selection.
  258. *
  259. * @returns {module:engine/view/position~Position|null}
  260. */
  261. getFirstPosition() {
  262. const firstRange = this.getFirstRange();
  263. return firstRange ? firstRange.start.clone() : null;
  264. }
  265. /**
  266. * Returns copy of the last position in the selection. Last position is the position that
  267. * {@link module:engine/view/position~Position#isAfter is after} any other position in the selection ranges.
  268. * Returns `null` if no ranges are added to selection.
  269. *
  270. * @returns {module:engine/view/position~Position|null}
  271. */
  272. getLastPosition() {
  273. const lastRange = this.getLastRange();
  274. return lastRange ? lastRange.end.clone() : null;
  275. }
  276. /**
  277. * Checks whether, this selection is equal to given selection. Selections are equal if they have same directions,
  278. * same number of ranges and all ranges from one selection equal to a range from other selection.
  279. *
  280. * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection
  281. * Selection to compare with.
  282. * @returns {Boolean} `true` if selections are equal, `false` otherwise.
  283. */
  284. isEqual( otherSelection ) {
  285. if ( this.isFake != otherSelection.isFake ) {
  286. return false;
  287. }
  288. if ( this.isFake && this.fakeSelectionLabel != otherSelection.fakeSelectionLabel ) {
  289. return false;
  290. }
  291. if ( this.rangeCount != otherSelection.rangeCount ) {
  292. return false;
  293. } else if ( this.rangeCount === 0 ) {
  294. return true;
  295. }
  296. if ( !this.anchor.isEqual( otherSelection.anchor ) || !this.focus.isEqual( otherSelection.focus ) ) {
  297. return false;
  298. }
  299. for ( const thisRange of this._ranges ) {
  300. let found = false;
  301. for ( const otherRange of otherSelection._ranges ) {
  302. if ( thisRange.isEqual( otherRange ) ) {
  303. found = true;
  304. break;
  305. }
  306. }
  307. if ( !found ) {
  308. return false;
  309. }
  310. }
  311. return true;
  312. }
  313. /**
  314. * Checks whether this selection is similar to given selection. Selections are similar if they have same directions, same
  315. * number of ranges, and all {@link module:engine/view/range~Range#getTrimmed trimmed} ranges from one selection are
  316. * equal to any trimmed range from other selection.
  317. *
  318. * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection
  319. * Selection to compare with.
  320. * @returns {Boolean} `true` if selections are similar, `false` otherwise.
  321. */
  322. isSimilar( otherSelection ) {
  323. if ( this.isBackward != otherSelection.isBackward ) {
  324. return false;
  325. }
  326. const numOfRangesA = count( this.getRanges() );
  327. const numOfRangesB = count( otherSelection.getRanges() );
  328. // If selections have different number of ranges, they cannot be similar.
  329. if ( numOfRangesA != numOfRangesB ) {
  330. return false;
  331. }
  332. // If both selections have no ranges, they are similar.
  333. if ( numOfRangesA == 0 ) {
  334. return true;
  335. }
  336. // Check if each range in one selection has a similar range in other selection.
  337. for ( let rangeA of this.getRanges() ) {
  338. rangeA = rangeA.getTrimmed();
  339. let found = false;
  340. for ( let rangeB of otherSelection.getRanges() ) {
  341. rangeB = rangeB.getTrimmed();
  342. if ( rangeA.start.isEqual( rangeB.start ) && rangeA.end.isEqual( rangeB.end ) ) {
  343. found = true;
  344. break;
  345. }
  346. }
  347. // For `rangeA`, neither range in `otherSelection` was similar. So selections are not similar.
  348. if ( !found ) {
  349. return false;
  350. }
  351. }
  352. // There were no ranges that weren't matched. Selections are similar.
  353. return true;
  354. }
  355. /**
  356. * Returns the selected element. {@link module:engine/view/element~Element Element} is considered as selected if there is only
  357. * one range in the selection, and that range contains exactly one element.
  358. * Returns `null` if there is no selected element.
  359. *
  360. * @returns {module:engine/view/element~Element|null}
  361. */
  362. getSelectedElement() {
  363. if ( this.rangeCount !== 1 ) {
  364. return null;
  365. }
  366. return this.getFirstRange().getContainedElement();
  367. }
  368. /**
  369. * Sets this selection's ranges and direction to the specified location based on the given
  370. * {@link module:engine/view/selection~Selectable selectable}.
  371. *
  372. * // Sets selection to the given range.
  373. * const range = writer.createRange( start, end );
  374. * selection.setTo( range );
  375. *
  376. * // Sets selection to given ranges.
  377. * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
  378. * selection.setTo( range );
  379. *
  380. * // Sets selection to the other selection.
  381. * const otherSelection = writer.createSelection();
  382. * selection.setTo( otherSelection );
  383. *
  384. * // Sets selection to contents of DocumentSelection.
  385. * selection.setTo( editor.editing.view.document.selection );
  386. *
  387. * // Sets collapsed selection at the given position.
  388. * const position = writer.createPositionAt( root, path );
  389. * selection.setTo( position );
  390. *
  391. * // Sets collapsed selection at the position of given item and offset.
  392. * selection.setTo( paragraph, offset );
  393. *
  394. * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
  395. * that element and ends after the last child of that element.
  396. *
  397. * selection.setTo( paragraph, 'in' );
  398. *
  399. * Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
  400. *
  401. * selection.setTo( paragraph, 'on' );
  402. *
  403. * // Clears selection. Removes all ranges.
  404. * selection.setTo( null );
  405. *
  406. * `Selection#setTo()` method allow passing additional options (`backward`, `fake` and `label`) as the last argument.
  407. *
  408. * // Sets selection as backward.
  409. * selection.setTo( range, { backward: true } );
  410. *
  411. * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
  412. * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
  413. * represented in other way, for example by applying proper CSS class.
  414. *
  415. * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
  416. * (and be properly handled by screen readers).
  417. *
  418. * // Creates fake selection with label.
  419. * selection.setTo( range, { fake: true, label: 'foo' } );
  420. *
  421. * @fires change
  422. * @param {module:engine/view/selection~Selectable} selectable
  423. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
  424. * @param {Object} [options]
  425. * @param {Boolean} [options.backward] Sets this selection instance to be backward.
  426. * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
  427. * @param {String} [options.label] Label for the fake selection.
  428. */
  429. setTo( selectable, placeOrOffset, options ) {
  430. if ( selectable === null ) {
  431. this._setRanges( [] );
  432. this._setFakeOptions( placeOrOffset );
  433. } else if ( selectable instanceof Selection || selectable instanceof DocumentSelection ) {
  434. this._setRanges( selectable.getRanges(), selectable.isBackward );
  435. this._setFakeOptions( { fake: selectable.isFake, label: selectable.fakeSelectionLabel } );
  436. } else if ( selectable instanceof Range ) {
  437. this._setRanges( [ selectable ], placeOrOffset && placeOrOffset.backward );
  438. this._setFakeOptions( placeOrOffset );
  439. } else if ( selectable instanceof Position ) {
  440. this._setRanges( [ new Range( selectable ) ] );
  441. this._setFakeOptions( placeOrOffset );
  442. } else if ( selectable instanceof Node ) {
  443. const backward = !!options && !!options.backward;
  444. let range;
  445. if ( placeOrOffset === undefined ) {
  446. /**
  447. * selection.setTo requires the second parameter when the first parameter is a node.
  448. *
  449. * @error view-selection-setTo-required-second-parameter
  450. */
  451. throw new CKEditorError(
  452. 'view-selection-setTo-required-second-parameter: ' +
  453. 'selection.setTo requires the second parameter when the first parameter is a node.',
  454. this
  455. );
  456. } else if ( placeOrOffset == 'in' ) {
  457. range = Range._createIn( selectable );
  458. } else if ( placeOrOffset == 'on' ) {
  459. range = Range._createOn( selectable );
  460. } else {
  461. range = new Range( Position._createAt( selectable, placeOrOffset ) );
  462. }
  463. this._setRanges( [ range ], backward );
  464. this._setFakeOptions( options );
  465. } else if ( isIterable( selectable ) ) {
  466. // We assume that the selectable is an iterable of ranges.
  467. // Array.from() is used to prevent setting ranges to the old iterable
  468. this._setRanges( selectable, placeOrOffset && placeOrOffset.backward );
  469. this._setFakeOptions( placeOrOffset );
  470. } else {
  471. /**
  472. * Cannot set selection to given place.
  473. *
  474. * @error view-selection-setTo-not-selectable
  475. */
  476. throw new CKEditorError( 'view-selection-setTo-not-selectable: Cannot set selection to given place.', this );
  477. }
  478. this.fire( 'change' );
  479. }
  480. /**
  481. * Moves {@link #focus} to the specified location.
  482. *
  483. * The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
  484. * parameters.
  485. *
  486. * @fires change
  487. * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
  488. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  489. * first parameter is a {@link module:engine/view/item~Item view item}.
  490. */
  491. setFocus( itemOrPosition, offset ) {
  492. if ( this.anchor === null ) {
  493. /**
  494. * Cannot set selection focus if there are no ranges in selection.
  495. *
  496. * @error view-selection-setFocus-no-ranges
  497. */
  498. throw new CKEditorError(
  499. 'view-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.',
  500. this
  501. );
  502. }
  503. const newFocus = Position._createAt( itemOrPosition, offset );
  504. if ( newFocus.compareWith( this.focus ) == 'same' ) {
  505. return;
  506. }
  507. const anchor = this.anchor;
  508. this._ranges.pop();
  509. if ( newFocus.compareWith( anchor ) == 'before' ) {
  510. this._addRange( new Range( newFocus, anchor ), true );
  511. } else {
  512. this._addRange( new Range( anchor, newFocus ) );
  513. }
  514. this.fire( 'change' );
  515. }
  516. /**
  517. * Checks whether this object is of the given type.
  518. *
  519. * selection.is( 'selection' ); // -> true
  520. * selection.is( 'view:selection' ); // -> true
  521. *
  522. * selection.is( 'model:selection' ); // -> false
  523. * selection.is( 'element' ); // -> false
  524. * selection.is( 'range' ); // -> false
  525. *
  526. * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
  527. *
  528. * @param {String} type
  529. * @returns {Boolean}
  530. */
  531. is( type ) {
  532. return type === 'selection' || type === 'view:selection';
  533. }
  534. /**
  535. * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
  536. * is treated like the last added range and is used to set {@link #anchor anchor} and {@link #focus focus}.
  537. * Accepts a flag describing in which way the selection is made.
  538. *
  539. * @private
  540. * @param {Iterable.<module:engine/view/range~Range>} newRanges Iterable object of ranges to set.
  541. * @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end
  542. * (`false`) or backward - from end to start (`true`). Defaults to `false`.
  543. */
  544. _setRanges( newRanges, isLastBackward = false ) {
  545. // New ranges should be copied to prevent removing them by setting them to `[]` first.
  546. // Only applies to situations when selection is set to the same selection or same selection's ranges.
  547. newRanges = Array.from( newRanges );
  548. this._ranges = [];
  549. for ( const range of newRanges ) {
  550. this._addRange( range );
  551. }
  552. this._lastRangeBackward = !!isLastBackward;
  553. }
  554. /**
  555. * Sets this selection instance to be marked as `fake`. A fake selection does not render as browser native selection
  556. * over selected elements and is hidden to the user. This way, no native selection UI artifacts are displayed to
  557. * the user and selection over elements can be represented in other way, for example by applying proper CSS class.
  558. *
  559. * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM (and be
  560. * properly handled by screen readers).
  561. *
  562. * @private
  563. * @param {Object} [options] Options.
  564. * @param {Boolean} [options.fake] If set to true selection will be marked as `fake`.
  565. * @param {String} [options.label=''] Fake selection label.
  566. */
  567. _setFakeOptions( options = {} ) {
  568. this._isFake = !!options.fake;
  569. this._fakeSelectionLabel = options.fake ? options.label || '' : '';
  570. }
  571. /**
  572. * Adds a range to the selection. Added range is copied. This means that passed range is not saved in the
  573. * selection instance and you can safely operate on it.
  574. *
  575. * Accepts a flag describing in which way the selection is made - passed range might be selected from
  576. * {@link module:engine/view/range~Range#start start} to {@link module:engine/view/range~Range#end end}
  577. * or from {@link module:engine/view/range~Range#end end} to {@link module:engine/view/range~Range#start start}.
  578. * The flag is used to set {@link #anchor anchor} and {@link #focus focus} properties.
  579. *
  580. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
  581. * with ranges already stored in Selection instance.
  582. *
  583. * @private
  584. * @fires change
  585. * @param {module:engine/view/range~Range} range
  586. * @param {Boolean} [isBackward]
  587. */
  588. _addRange( range, isBackward = false ) {
  589. if ( !( range instanceof Range ) ) {
  590. /**
  591. * Selection range set to an object that is not an instance of {@link module:engine/view/range~Range}.
  592. *
  593. * @error view-selection-add-range-not-range
  594. */
  595. throw new CKEditorError(
  596. 'view-selection-add-range-not-range: ' +
  597. 'Selection range set to an object that is not an instance of view.Range',
  598. this
  599. );
  600. }
  601. this._pushRange( range );
  602. this._lastRangeBackward = !!isBackward;
  603. }
  604. /**
  605. * Adds range to selection - creates copy of given range so it can be safely used and modified.
  606. *
  607. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
  608. * with ranges already stored in selection instance.
  609. *
  610. * @private
  611. * @param {module:engine/view/range~Range} range
  612. */
  613. _pushRange( range ) {
  614. for ( const storedRange of this._ranges ) {
  615. if ( range.isIntersecting( storedRange ) ) {
  616. /**
  617. * Trying to add a range that intersects with another range from selection.
  618. *
  619. * @error view-selection-range-intersects
  620. * @param {module:engine/view/range~Range} addedRange Range that was added to the selection.
  621. * @param {module:engine/view/range~Range} intersectingRange Range from selection that intersects with `addedRange`.
  622. */
  623. throw new CKEditorError(
  624. 'view-selection-range-intersects: Trying to add a range that intersects with another range from selection.',
  625. this,
  626. { addedRange: range, intersectingRange: storedRange }
  627. );
  628. }
  629. }
  630. this._ranges.push( new Range( range.start, range.end ) );
  631. }
  632. /**
  633. * Fired whenever selection ranges are changed through {@link ~Selection Selection API}.
  634. *
  635. * @event change
  636. */
  637. }
  638. mix( Selection, EmitterMixin );
  639. /**
  640. * An entity that is used to set selection.
  641. *
  642. * See also {@link module:engine/view/selection~Selection#setTo}
  643. *
  644. * @typedef {
  645. * module:engine/view/selection~Selection|
  646. * module:engine/view/documentselection~DocumentSelection|
  647. * module:engine/view/position~Position|
  648. * Iterable.<module:engine/view/range~Range>|
  649. * module:engine/view/range~Range|
  650. * module:engine/view/item~Item|
  651. * null
  652. * } module:engine/view/selection~Selectable
  653. */