8
0

selection.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/view/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 Element from './element';
  14. /**
  15. * Class representing selection in tree view.
  16. *
  17. * Selection can consist of {@link module:engine/view/range~Range ranges} that can be added using
  18. * {@link module:engine/view/selection~Selection#addRange addRange}
  19. * and {@link module:engine/view/selection~Selection#setRanges setRanges} methods.
  20. * Both methods create copies of provided ranges and store those copies internally. Further modifications to passed
  21. * ranges will not change selection's state.
  22. * Selection's ranges can be obtained via {@link module:engine/view/selection~Selection#getRanges getRanges},
  23. * {@link module:engine/view/selection~Selection#getFirstRange getFirstRange}
  24. * and {@link module:engine/view/selection~Selection#getLastRange getLastRange}
  25. * methods, which return copies of ranges stored inside selection. Modifications made on these copies will not change
  26. * selection's state. Similar situation occurs when getting {@link module:engine/view/selection~Selection#anchor anchor},
  27. * {@link module:engine/view/selection~Selection#focus focus}, {@link module:engine/view/selection~Selection#getFirstPosition first} and
  28. * {@link module:engine/view/selection~Selection#getLastPosition last} positions - all will return copies of requested positions.
  29. */
  30. export default class Selection {
  31. /**
  32. * Creates new selection instance.
  33. */
  34. constructor() {
  35. /**
  36. * Stores all ranges that are selected.
  37. *
  38. * @protected
  39. * @member {Array.<module:engine/view/range~Range>}
  40. */
  41. this._ranges = [];
  42. /**
  43. * Specifies whether the last added range was added as a backward or forward range.
  44. *
  45. * @protected
  46. * @member {Boolean}
  47. */
  48. this._lastRangeBackward = false;
  49. /**
  50. * Specifies whether selection instance is fake.
  51. *
  52. * @private
  53. * @member {Boolean}
  54. */
  55. this._isFake = false;
  56. /**
  57. * Fake selection's label.
  58. *
  59. * @private
  60. * @member {String}
  61. */
  62. this._fakeSelectionLabel = '';
  63. }
  64. /**
  65. * Sets this selection instance to be marked as `fake`. A fake selection does not render as browser native selection
  66. * over selected elements and is hidden to the user. This way, no native selection UI artifacts are displayed to
  67. * the user and selection over elements can be represented in other way, for example by applying proper CSS class.
  68. *
  69. * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM (and be
  70. * properly handled by screen readers).
  71. *
  72. * @fires change
  73. * @param {Boolean} [value=true] If set to true selection will be marked as `fake`.
  74. * @param {Object} [options] Additional options.
  75. * @param {String} [options.label=''] Fake selection label.
  76. */
  77. setFake( value = true, options = {} ) {
  78. this._isFake = value;
  79. this._fakeSelectionLabel = value ? options.label || '' : '';
  80. this.fire( 'change' );
  81. }
  82. /**
  83. * Returns true if selection instance is marked as `fake`.
  84. *
  85. * @see #setFake
  86. * @returns {Boolean}
  87. */
  88. get isFake() {
  89. return this._isFake;
  90. }
  91. /**
  92. * Returns fake selection label.
  93. *
  94. * @see #setFake
  95. * @returns {String}
  96. */
  97. get fakeSelectionLabel() {
  98. return this._fakeSelectionLabel;
  99. }
  100. /**
  101. * Selection anchor. Anchor may be described as a position where the selection starts. Together with
  102. * {@link #focus focus} they define the direction of selection, which is important
  103. * when expanding/shrinking selection. Anchor is always the start or end of the most recent added range.
  104. * It may be a bit unintuitive when there are multiple ranges in selection.
  105. *
  106. * @see #focus
  107. * @type {module:engine/view/position~Position}
  108. */
  109. get anchor() {
  110. if ( !this._ranges.length ) {
  111. return null;
  112. }
  113. const range = this._ranges[ this._ranges.length - 1 ];
  114. const anchor = this._lastRangeBackward ? range.end : range.start;
  115. return Position.createFromPosition( anchor );
  116. }
  117. /**
  118. * Selection focus. Focus is a position where the selection ends.
  119. *
  120. * @see #anchor
  121. * @type {module:engine/view/position~Position}
  122. */
  123. get focus() {
  124. if ( !this._ranges.length ) {
  125. return null;
  126. }
  127. const range = this._ranges[ this._ranges.length - 1 ];
  128. const focus = this._lastRangeBackward ? range.start : range.end;
  129. return Position.createFromPosition( focus );
  130. }
  131. /**
  132. * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
  133. * collapsed.
  134. *
  135. * @type {Boolean}
  136. */
  137. get isCollapsed() {
  138. return this.rangeCount === 1 && this._ranges[ 0 ].isCollapsed;
  139. }
  140. /**
  141. * Returns number of ranges in selection.
  142. *
  143. * @type {Number}
  144. */
  145. get rangeCount() {
  146. return this._ranges.length;
  147. }
  148. /**
  149. * Specifies whether the {@link #focus} precedes {@link #anchor}.
  150. *
  151. * @type {Boolean}
  152. */
  153. get isBackward() {
  154. return !this.isCollapsed && this._lastRangeBackward;
  155. }
  156. /**
  157. * {@link module:engine/view/editableelement~EditableElement EditableElement} instance that contains this selection, or `null`
  158. * if the selection is not inside an editable element.
  159. *
  160. * @type {module:engine/view/editableelement~EditableElement|null}
  161. */
  162. get editableElement() {
  163. if ( this.anchor ) {
  164. return this.anchor.editableElement;
  165. }
  166. return null;
  167. }
  168. /**
  169. * Adds a range to the selection. Added range is copied. This means that passed range is not saved in the
  170. * selection instance and you can safely operate on it.
  171. *
  172. * Accepts a flag describing in which way the selection is made - passed range might be selected from
  173. * {@link module:engine/view/range~Range#start start} to {@link module:engine/view/range~Range#end end}
  174. * or from {@link module:engine/view/range~Range#end end} to {@link module:engine/view/range~Range#start start}.
  175. * The flag is used to set {@link #anchor anchor} and {@link #focus focus} properties.
  176. *
  177. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
  178. * with ranges already stored in Selection instance.
  179. *
  180. * @fires change
  181. * @param {module:engine/view/range~Range} range
  182. */
  183. addRange( range, isBackward ) {
  184. if ( !( range instanceof Range ) ) {
  185. throw new CKEditorError( 'view-selection-invalid-range: Invalid Range.' );
  186. }
  187. this._pushRange( range );
  188. this._lastRangeBackward = !!isBackward;
  189. this.fire( 'change' );
  190. }
  191. /**
  192. * Returns an iterator that contains copies of all ranges added to the selection.
  193. *
  194. * @returns {Iterator.<module:engine/view/range~Range>}
  195. */
  196. *getRanges() {
  197. for ( let range of this._ranges ) {
  198. yield Range.createFromRange( range );
  199. }
  200. }
  201. /**
  202. * Returns copy of the first range in the selection. First range is the one which
  203. * {@link module:engine/view/range~Range#start start} position {@link module:engine/view/position~Position#isBefore is before} start
  204. * position of all other ranges (not to confuse with the first range added to the selection).
  205. * Returns `null` if no ranges are added to selection.
  206. *
  207. * @returns {module:engine/view/range~Range|null}
  208. */
  209. getFirstRange() {
  210. let first = null;
  211. for ( let range of this._ranges ) {
  212. if ( !first || range.start.isBefore( first.start ) ) {
  213. first = range;
  214. }
  215. }
  216. return first ? Range.createFromRange( first ) : null;
  217. }
  218. /**
  219. * Returns copy of the last range in the selection. Last range is the one which {@link module:engine/view/range~Range#end end}
  220. * position {@link module:engine/view/position~Position#isAfter is after} end position of all other ranges (not to confuse
  221. * with the last range added to the selection). Returns `null` if no ranges are added to selection.
  222. *
  223. * @returns {module:engine/view/range~Range|null}
  224. */
  225. getLastRange() {
  226. let last = null;
  227. for ( let range of this._ranges ) {
  228. if ( !last || range.end.isAfter( last.end ) ) {
  229. last = range;
  230. }
  231. }
  232. return last ? Range.createFromRange( last ) : null;
  233. }
  234. /**
  235. * Returns copy of the first position in the selection. First position is the position that
  236. * {@link module:engine/view/position~Position#isBefore is before} any other position in the selection ranges.
  237. * Returns `null` if no ranges are added to selection.
  238. *
  239. * @returns {module:engine/view/position~Position|null}
  240. */
  241. getFirstPosition() {
  242. const firstRange = this.getFirstRange();
  243. return firstRange ? Position.createFromPosition( firstRange.start ) : null;
  244. }
  245. /**
  246. * Returns copy of the last position in the selection. Last position is the position that
  247. * {@link module:engine/view/position~Position#isAfter is after} any other position in the selection ranges.
  248. * Returns `null` if no ranges are added to selection.
  249. *
  250. * @returns {module:engine/view/position~Position|null}
  251. */
  252. getLastPosition() {
  253. const lastRange = this.getLastRange();
  254. return lastRange ? Position.createFromPosition( lastRange.end ) : null;
  255. }
  256. /**
  257. * Checks whether, this selection is equal to given selection. Selections are equal if they have same directions,
  258. * same number of ranges and all ranges from one selection equal to a range from other selection.
  259. *
  260. * @param {module:engine/view/selection~Selection} otherSelection Selection to compare with.
  261. * @returns {Boolean} `true` if selections are equal, `false` otherwise.
  262. */
  263. isEqual( otherSelection ) {
  264. if ( this.isFake != otherSelection.isFake ) {
  265. return false;
  266. }
  267. if ( this.isFake && this.fakeSelectionLabel != otherSelection.fakeSelectionLabel ) {
  268. return false;
  269. }
  270. if ( this.rangeCount != otherSelection.rangeCount ) {
  271. return false;
  272. } else if ( this.rangeCount === 0 ) {
  273. return true;
  274. }
  275. if ( !this.anchor.isEqual( otherSelection.anchor ) || !this.focus.isEqual( otherSelection.focus ) ) {
  276. return false;
  277. }
  278. for ( let thisRange of this._ranges ) {
  279. let found = false;
  280. for ( let otherRange of otherSelection._ranges ) {
  281. if ( thisRange.isEqual( otherRange ) ) {
  282. found = true;
  283. break;
  284. }
  285. }
  286. if ( !found ) {
  287. return false;
  288. }
  289. }
  290. return true;
  291. }
  292. /**
  293. * Removes all ranges that were added to the selection.
  294. *
  295. * @fires change
  296. */
  297. removeAllRanges() {
  298. if ( this._ranges.length ) {
  299. this._ranges = [];
  300. this.fire( 'change' );
  301. }
  302. }
  303. /**
  304. * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
  305. * is treated like the last added range and is used to set {@link #anchor anchor} and {@link #focus focus}.
  306. * Accepts a flag describing in which way the selection is made (see {@link #addRange addRange}).
  307. *
  308. * @fires change
  309. * @param {Array.<module:engine/view/range~Range>} newRanges Array of ranges to set.
  310. * @param {Boolean} [isLastBackward] Flag describing if last added range was selected forward - from start to end
  311. * (`false`) or backward - from end to start (`true`). Defaults to `false`.
  312. */
  313. setRanges( newRanges, isLastBackward ) {
  314. this._ranges = [];
  315. for ( let range of newRanges ) {
  316. if ( !( range instanceof Range ) ) {
  317. throw new CKEditorError( 'view-selection-invalid-range: Invalid Range.' );
  318. }
  319. this._pushRange( range );
  320. }
  321. this._lastRangeBackward = !!isLastBackward;
  322. this.fire( 'change' );
  323. }
  324. /**
  325. * Sets this selection's ranges and direction to the ranges and direction of the given selection.
  326. *
  327. * @param {module:engine/view/selection~Selection} otherSelection
  328. */
  329. setTo( otherSelection ) {
  330. this._isFake = otherSelection._isFake;
  331. this._fakeSelectionLabel = otherSelection._fakeSelectionLabel;
  332. this.setRanges( otherSelection.getRanges(), otherSelection.isBackward );
  333. }
  334. /**
  335. * Sets collapsed selection in the specified location.
  336. *
  337. * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
  338. *
  339. * @fires change
  340. * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
  341. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  342. * first parameter is a {@link module:engine/view/item~Item view item}.
  343. */
  344. collapse( itemOrPosition, offset ) {
  345. const pos = Position.createAt( itemOrPosition, offset );
  346. const range = new Range( pos, pos );
  347. this.setRanges( [ range ] );
  348. }
  349. /**
  350. * Collapses selection to the selection's {@link #getFirstPosition first position}.
  351. * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
  352. * inside selection.
  353. *
  354. * @fires change
  355. */
  356. collapseToStart() {
  357. const startPosition = this.getFirstPosition();
  358. if ( startPosition !== null ) {
  359. this.setRanges( [ new Range( startPosition, startPosition ) ] );
  360. }
  361. }
  362. /**
  363. * Collapses selection to the selection's {@link #getLastPosition last position}.
  364. * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
  365. * inside selection.
  366. *
  367. * @fires change
  368. */
  369. collapseToEnd() {
  370. const endPosition = this.getLastPosition();
  371. if ( endPosition !== null ) {
  372. this.setRanges( [ new Range( endPosition, endPosition ) ] );
  373. }
  374. }
  375. /**
  376. * Sets {@link #focus} to the specified location.
  377. *
  378. * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
  379. *
  380. * @fires change:range
  381. * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
  382. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  383. * first parameter is a {@link module:engine/view/item~Item view item}.
  384. */
  385. setFocus( itemOrPosition, offset ) {
  386. if ( this.anchor === null ) {
  387. /**
  388. * Cannot set selection focus if there are no ranges in selection.
  389. *
  390. * @error view-selection-setFocus-no-ranges
  391. */
  392. throw new CKEditorError( 'view-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.' );
  393. }
  394. const newFocus = Position.createAt( itemOrPosition, offset );
  395. if ( newFocus.compareWith( this.focus ) == 'same' ) {
  396. return;
  397. }
  398. const anchor = this.anchor;
  399. this._ranges.pop();
  400. if ( newFocus.compareWith( anchor ) == 'before' ) {
  401. this.addRange( new Range( newFocus, anchor ), true );
  402. } else {
  403. this.addRange( new Range( anchor, newFocus ) );
  404. }
  405. }
  406. /**
  407. * Returns the selected element. {@link module:engine/view/element~Element Element} is considered as selected if there is only
  408. * one range in the selection, and that range contains exactly one element.
  409. * Returns `null` if there is no selected element.
  410. *
  411. * @returns {module:engine/view/element~Element|null}
  412. */
  413. getSelectedElement() {
  414. if ( this.rangeCount !== 1 ) {
  415. return null;
  416. }
  417. const range = this.getFirstRange();
  418. const nodeAfterStart = range.start.nodeAfter;
  419. const nodeBeforeEnd = range.end.nodeBefore;
  420. return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
  421. }
  422. /**
  423. * Creates and returns an instance of `Selection` that is a clone of given selection, meaning that it has same
  424. * ranges and same direction as this selection.
  425. *
  426. * @params {module:engine/view/selection~Selection} otherSelection Selection to be cloned.
  427. * @returns {module:engine/view/selection~Selection} `Selection` instance that is a clone of given selection.
  428. */
  429. static createFromSelection( otherSelection ) {
  430. const selection = new Selection();
  431. selection.setTo( otherSelection );
  432. return selection;
  433. }
  434. /**
  435. * Adds range to selection - creates copy of given range so it can be safely used and modified.
  436. *
  437. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
  438. * with ranges already stored in selection instance.
  439. *
  440. * @private
  441. * @param {module:engine/view/range~Range} range
  442. */
  443. _pushRange( range ) {
  444. for ( let storedRange of this._ranges ) {
  445. if ( range.isIntersecting( storedRange ) ) {
  446. /**
  447. * Trying to add a range that intersects with another range from selection.
  448. *
  449. * @error selection-range-intersects
  450. * @param {module:engine/view/range~Range} addedRange Range that was added to the selection.
  451. * @param {module:engine/view/range~Range} intersectingRange Range from selection that intersects with `addedRange`.
  452. */
  453. throw new CKEditorError(
  454. 'view-selection-range-intersects: Trying to add a range that intersects with another range from selection.',
  455. { addedRange: range, intersectingRange: storedRange }
  456. );
  457. }
  458. }
  459. this._ranges.push( Range.createFromRange( range ) );
  460. }
  461. }
  462. mix( Selection, EmitterMixin );
  463. /**
  464. * Fired whenever selection ranges are changed through {@link ~Selection Selection API}.
  465. *
  466. * @event change
  467. */
  468. /**
  469. * @event change:range
  470. */