8
0

selection.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Position from './position.js';
  6. import Range from './range.js';
  7. import EmitterMixin from '../../utils/emittermixin.js';
  8. import CKEditorError from '../../utils/ckeditorerror.js';
  9. import mix from '../../utils/mix.js';
  10. import toMap from '../../utils/tomap.js';
  11. import mapsEqual from '../../utils/mapsequal.js';
  12. /**
  13. * `Selection` is a group of {@link engine.model.Range ranges} which has a direction specified by
  14. * {@link engine.model.Selection#anchor anchor} and {@link engine.model.Selection#focus focus}. Additionally,
  15. * `Selection` may have it's own attributes.
  16. *
  17. * @memberOf engine.model
  18. */
  19. export default class Selection {
  20. /**
  21. * Creates an empty selection.
  22. */
  23. constructor() {
  24. /**
  25. * Specifies whether the last added range was added as a backward or forward range.
  26. *
  27. * @private
  28. * @member {Boolean} engine.model.Selection#_lastRangeBackward
  29. */
  30. this._lastRangeBackward = false;
  31. /**
  32. * Stores selection ranges.
  33. *
  34. * @protected
  35. * @member {Array.<engine.model.Range>} engine.model.Selection#_ranges
  36. */
  37. this._ranges = [];
  38. /**
  39. * List of attributes set on current selection.
  40. *
  41. * @protected
  42. * @member {Map} engine.model.LiveSelection#_attrs
  43. */
  44. this._attrs = new Map();
  45. }
  46. /**
  47. * Selection anchor. Anchor may be described as a position where the most recent part of the selection starts.
  48. * Together with {@link engine.model.Selection#focus} they define the direction of selection, which is important
  49. * when expanding/shrinking selection. Anchor is always {@link engine.model.Range#start start} or
  50. * {@link engine.model.Range#end end} position of the most recently added range.
  51. *
  52. * Is set to `null` if there are no ranges in selection.
  53. *
  54. * @see engine.model.Selection#focus
  55. * @readonly
  56. * @type {engine.model.Position|null}
  57. */
  58. get anchor() {
  59. if ( this._ranges.length > 0 ) {
  60. const range = this._ranges[ this._ranges.length - 1 ];
  61. return this._lastRangeBackward ? range.end : range.start;
  62. }
  63. return null;
  64. }
  65. /**
  66. * Selection focus. Focus is a position where the selection ends.
  67. *
  68. * Is set to `null` if there are no ranges in selection.
  69. *
  70. * @see engine.model.Selection#anchor
  71. * @readonly
  72. * @type {engine.model.Position|null}
  73. */
  74. get focus() {
  75. if ( this._ranges.length > 0 ) {
  76. const range = this._ranges[ this._ranges.length - 1 ];
  77. return this._lastRangeBackward ? range.start : range.end;
  78. }
  79. return null;
  80. }
  81. /**
  82. * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
  83. * collapsed.
  84. *
  85. * @readonly
  86. * @type {Boolean}
  87. */
  88. get isCollapsed() {
  89. const length = this._ranges.length;
  90. if ( length === 1 ) {
  91. return this._ranges[ 0 ].isCollapsed;
  92. } else {
  93. return false;
  94. }
  95. }
  96. /**
  97. * Returns number of ranges in selection.
  98. *
  99. * @type {Number}
  100. */
  101. get rangeCount() {
  102. return this._ranges.length;
  103. }
  104. /**
  105. * Specifies whether the {@link engine.model.Selection#focus} precedes {@link engine.model.Selection#anchor}.
  106. *
  107. * @type {Boolean}
  108. */
  109. get isBackward() {
  110. return !this.isCollapsed && this._lastRangeBackward;
  111. }
  112. /**
  113. * Checks whether this selection is equal to given selection. Selections are equal if they have same directions,
  114. * same number of ranges and all ranges from one selection equal to a range from other selection.
  115. *
  116. * @param {engine.model.Selection} otherSelection Selection to compare with.
  117. * @returns {Boolean} `true` if selections are equal, `false` otherwise.
  118. */
  119. isEqual( otherSelection ) {
  120. if ( !this.anchor.isEqual( otherSelection.anchor ) || !this.focus.isEqual( otherSelection.focus ) ) {
  121. return false;
  122. }
  123. if ( this.rangeCount != otherSelection.rangeCount ) {
  124. return false;
  125. }
  126. // Every range from this selection...
  127. return Array.from( this.getRanges() ).every( ( rangeA ) => {
  128. // ...Has a range in other selection...
  129. return Array.from( otherSelection.getRanges() ).some( ( rangeB ) => {
  130. // That it is equal to.
  131. return rangeA.isEqual( rangeB );
  132. } );
  133. } );
  134. }
  135. /**
  136. * Returns an iterator that iterates over copies of selection ranges.
  137. *
  138. * @returns {Iterator.<engine.model.Range>}
  139. */
  140. *getRanges() {
  141. for ( let range of this._ranges ) {
  142. yield Range.createFromRange( range );
  143. }
  144. }
  145. /**
  146. * Returns a copy of the first range in the selection. First range is the one which {@link engine.model.Range#start start} position
  147. * {@link engine.model.Position#isBefore is before} start position of all other ranges (not to confuse with the first range
  148. * added to the selection).
  149. *
  150. * Returns `null` if there are no ranges in selection.
  151. *
  152. * @returns {engine.model.Range|null}
  153. */
  154. getFirstRange() {
  155. let first = null;
  156. for ( let range of this._ranges ) {
  157. if ( !first || range.start.isBefore( first.start ) ) {
  158. first = range;
  159. }
  160. }
  161. return first ? Range.createFromRange( first ) : null;
  162. }
  163. /**
  164. * Returns a copy of the last range in the selection. Last range is the one which {@link engine.model.Range#end end} position
  165. * {@link engine.model.Position#isAfter is after} end position of all other ranges (not to confuse with the range most
  166. * recently added to the selection).
  167. *
  168. * Returns `null` if there are no ranges in selection.
  169. *
  170. * @returns {engine.model.Range|null}
  171. */
  172. getLastRange() {
  173. let last = null;
  174. for ( let range of this._ranges ) {
  175. if ( !last || range.end.isAfter( last.end ) ) {
  176. last = range;
  177. }
  178. }
  179. return last ? Range.createFromRange( last ) : null;
  180. }
  181. /**
  182. * Returns the first position in the selection. First position is the position that {@link engine.model.Position#isBefore is before}
  183. * any other position in the selection.
  184. *
  185. * Returns `null` if there are no ranges in selection.
  186. *
  187. * @returns {engine.model.Position|null}
  188. */
  189. getFirstPosition() {
  190. const first = this.getFirstRange();
  191. return first ? Position.createFromPosition( first.start ) : null;
  192. }
  193. /**
  194. * Returns the last position in the selection. Last position is the position that {@link engine.model.Position#isAfter is after}
  195. * any other position in the selection.
  196. *
  197. * Returns `null` if there are no ranges in selection.
  198. *
  199. * @returns {engine.model.Position|null}
  200. */
  201. getLastPosition() {
  202. const lastRange = this.getLastRange();
  203. return lastRange ? Position.createFromPosition( lastRange.end ) : null;
  204. }
  205. /**
  206. * Adds a range to this selection. Added range is copied. This means that passed range is not saved in `Selection`
  207. * instance and operating on it will not change `Selection` state.
  208. *
  209. * Accepts a flag describing in which way the selection is made - passed range might be selected from
  210. * {@link engine.model.Range#start start} to {@link engine.model.Range#end end} or from {@link engine.model.Range#end end}
  211. * to {@link engine.model.Range#start start}. The flag is used to set {@link engine.model.Selection#anchor} and
  212. * {@link engine.model.Selection#focus} properties.
  213. *
  214. * @fires engine.model.Selection#change:range
  215. * @param {engine.model.Range} range Range to add.
  216. * @param {Boolean} [isBackward=false] Flag describing if added range was selected forward - from start to end (`false`)
  217. * or backward - from end to start (`true`).
  218. */
  219. addRange( range, isBackward = false ) {
  220. this._pushRange( range );
  221. this._lastRangeBackward = !!isBackward;
  222. this.fire( 'change:range', { directChange: true } );
  223. }
  224. /**
  225. * Removes all ranges that were added to the selection.
  226. *
  227. * @fires engine.model.Selection#change:range
  228. */
  229. removeAllRanges() {
  230. if ( this._ranges.length > 0 ) {
  231. this._removeAllRanges();
  232. this.fire( 'change:range', { directChange: true } );
  233. }
  234. }
  235. /**
  236. * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
  237. * is treated like the last added range and is used to set {@link engine.model.Selection#anchor} and
  238. * {@link engine.model.Selection#focus}. Accepts a flag describing in which direction the selection is made
  239. * (see {@link engine.model.Selection#addRange}).
  240. *
  241. * @fires engine.model.Selection#change:range
  242. * @param {Iterable.<engine.model.Range>} newRanges Ranges to set.
  243. * @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end (`false`)
  244. * or backward - from end to start (`true`).
  245. */
  246. setRanges( newRanges, isLastBackward = false ) {
  247. newRanges = Array.from( newRanges );
  248. // Check whether there is any range in new ranges set that is different than all already added ranges.
  249. const anyNewRange = newRanges.some( ( newRange ) => {
  250. if ( !( newRange instanceof Range ) ) {
  251. throw new CKEditorError( 'model-selection-added-not-range: Trying to add an object that is not an instance of Range.' );
  252. }
  253. return this._ranges.every( ( oldRange ) => {
  254. return !oldRange.isEqual( newRange );
  255. } );
  256. } );
  257. // Don't do anything if nothing changed.
  258. if ( newRanges.length === this._ranges.length && !anyNewRange ) {
  259. return;
  260. }
  261. this._removeAllRanges();
  262. for ( let range of newRanges ) {
  263. this._pushRange( range );
  264. }
  265. this._lastRangeBackward = !!isLastBackward;
  266. this.fire( 'change:range', { directChange: true } );
  267. }
  268. /**
  269. * Sets this selection's ranges and direction to the ranges and direction of the given selection.
  270. *
  271. * @param {engine.model.Selection} otherSelection
  272. */
  273. setTo( otherSelection ) {
  274. this.setRanges( otherSelection.getRanges(), otherSelection.isBackward );
  275. }
  276. /**
  277. * Sets collapsed selection in the specified location.
  278. *
  279. * The location can be specified in the same form as {@link engine.model.Position.createAt} parameters.
  280. *
  281. * @fires engine.model.Selection#change:range
  282. * @param {engine.model.Item|engine.model.Position} itemOrPosition
  283. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  284. * first parameter is a {@link engine.model.Item model item}.
  285. */
  286. collapse( itemOrPosition, offset ) {
  287. const pos = Position.createAt( itemOrPosition, offset );
  288. const range = new Range( pos, pos );
  289. this.setRanges( [ range ] );
  290. }
  291. /**
  292. * Collapses selection to the selection's {@link engine.model.Selection#getFirstPosition first position}.
  293. * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
  294. * inside selection.
  295. *
  296. * @fires engine.view.Selection#change
  297. */
  298. collapseToStart() {
  299. const startPosition = this.getFirstPosition();
  300. if ( startPosition !== null ) {
  301. this.setRanges( [ new Range( startPosition, startPosition ) ] );
  302. }
  303. }
  304. /**
  305. * Collapses selection to the selection's {@link engine.model.Selection#getLastPosition last position}.
  306. * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
  307. * inside selection.
  308. *
  309. * @fires engine.view.Selection#change
  310. */
  311. collapseToEnd() {
  312. const endPosition = this.getLastPosition();
  313. if ( endPosition !== null ) {
  314. this.setRanges( [ new Range( endPosition, endPosition ) ] );
  315. }
  316. }
  317. /**
  318. * Sets {@link engine.model.Selection#focus} to the specified location.
  319. *
  320. * The location can be specified in the same form as {@link engine.model.Position.createAt} parameters.
  321. *
  322. * @fires engine.model.Selection#change:range
  323. * @param {engine.model.Item|engine.model.Position} itemOrPosition
  324. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  325. * first parameter is a {@link engine.model.Item model item}.
  326. */
  327. setFocus( itemOrPosition, offset ) {
  328. if ( this.anchor === null ) {
  329. /**
  330. * Cannot set selection focus if there are no ranges in selection.
  331. *
  332. * @error model-selection-setFocus-no-ranges
  333. */
  334. throw new CKEditorError( 'model-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.' );
  335. }
  336. const newFocus = Position.createAt( itemOrPosition, offset );
  337. if ( newFocus.compareWith( this.focus ) == 'same' ) {
  338. return;
  339. }
  340. const anchor = this.anchor;
  341. if ( this._ranges.length ) {
  342. this._popRange();
  343. }
  344. if ( newFocus.compareWith( anchor ) == 'before' ) {
  345. this.addRange( new Range( newFocus, anchor ), true );
  346. } else {
  347. this.addRange( new Range( anchor, newFocus ) );
  348. }
  349. }
  350. /**
  351. * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
  352. *
  353. * @param {String} key Key of attribute to look for.
  354. * @returns {*} Attribute value or `undefined`.
  355. */
  356. getAttribute( key ) {
  357. return this._attrs.get( key );
  358. }
  359. /**
  360. * Returns iterator that iterates over this selection's attributes.
  361. *
  362. * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
  363. * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
  364. *
  365. * @returns {Iterable.<*>}
  366. */
  367. getAttributes() {
  368. return this._attrs.entries();
  369. }
  370. /**
  371. * Returns iterator that iterates over this selection's attribute keys.
  372. *
  373. * @returns {Iterator.<String>}
  374. */
  375. getAttributeKeys() {
  376. return this._attrs.keys();
  377. }
  378. /**
  379. * Checks if the selection has an attribute for given key.
  380. *
  381. * @param {String} key Key of attribute to check.
  382. * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
  383. */
  384. hasAttribute( key ) {
  385. return this._attrs.has( key );
  386. }
  387. /**
  388. * Removes all attributes from the selection.
  389. *
  390. * If there were any attributes in selection, fires the {@link engine.model.Selection#change} event with
  391. * removed attributes' keys.
  392. *
  393. * @fires engine.model.Selection#change:attribute
  394. */
  395. clearAttributes() {
  396. if ( this._attrs.size > 0 ) {
  397. const attributeKeys = Array.from( this._attrs.keys() );
  398. this._attrs.clear();
  399. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  400. }
  401. }
  402. /**
  403. * Removes an attribute with given key from the selection.
  404. *
  405. * If given attribute was set on the selection, fires the {@link engine.model.Selection#change} event with
  406. * removed attribute key.
  407. *
  408. * @fires engine.model.Selection#change:attribute
  409. * @param {String} key Key of attribute to remove.
  410. */
  411. removeAttribute( key ) {
  412. if ( this.hasAttribute( key ) ) {
  413. this._attrs.delete( key );
  414. this.fire( 'change:attribute', { attributeKeys: [ key ], directChange: true } );
  415. }
  416. }
  417. /**
  418. * Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
  419. *
  420. * If the attribute value has changed, fires the {@link engine.model.Selection#change} event with
  421. * the attribute key.
  422. *
  423. * @fires engine.model.Selection#change:attribute
  424. * @param {String} key Key of attribute to set.
  425. * @param {*} value Attribute value.
  426. */
  427. setAttribute( key, value ) {
  428. if ( this.getAttribute( key ) !== value ) {
  429. this._attrs.set( key, value );
  430. this.fire( 'change:attribute', { attributeKeys: [ key ], directChange: true } );
  431. }
  432. }
  433. /**
  434. * Removes all attributes from the selection and sets given attributes.
  435. *
  436. * If given set of attributes is different than set of attributes already added to selection, fires
  437. * {@link engine.model.Selection#change change event} with keys of attributes that changed.
  438. *
  439. * @fires engine.model.Selection#change:attribute
  440. * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
  441. */
  442. setAttributesTo( attrs ) {
  443. attrs = toMap( attrs );
  444. if ( !mapsEqual( attrs, this._attrs ) ) {
  445. // Create a set from keys of old and new attributes.
  446. const changed = new Set( Array.from( attrs.keys() ).concat( Array.from( this._attrs.keys() ) ) );
  447. for ( let [ key, value ] of attrs ) {
  448. // If the attribute remains unchanged, remove it from changed set.
  449. if ( this._attrs.get( key ) === value ) {
  450. changed.delete( key );
  451. }
  452. }
  453. this._attrs = attrs;
  454. this.fire( 'change:attribute', { attributeKeys: Array.from( changed ), directChange: true } );
  455. }
  456. }
  457. /**
  458. * Creates and returns an instance of `Selection` that is a clone of given selection, meaning that it has same
  459. * ranges and same direction as this selection.
  460. *
  461. * @params {engine.model.Selection} otherSelection Selection to be cloned.
  462. * @returns {engine.model.Selection} `Selection` instance that is a clone of given selection.
  463. */
  464. static createFromSelection( otherSelection ) {
  465. const selection = new this();
  466. selection.setTo( otherSelection );
  467. return selection;
  468. }
  469. /**
  470. * Adds given range to internal {@link engine.model.Selection#_ranges ranges array}. Throws an error
  471. * if given range is intersecting with any range that is already stored in this selection.
  472. *
  473. * @protected
  474. * @param {engine.model.Range} range Range to add.
  475. */
  476. _pushRange( range ) {
  477. if ( !( range instanceof Range ) ) {
  478. throw new CKEditorError( 'model-selection-added-not-range: Trying to add an object that is not an instance of Range.' );
  479. }
  480. this._checkRange( range );
  481. this._ranges.push( Range.createFromRange( range ) );
  482. }
  483. /**
  484. * Checks if given range intersects with ranges that are already in the selection. Throws an error if it does.
  485. *
  486. * @protected
  487. * @param {engine.model.Range} range Range to check.
  488. */
  489. _checkRange( range ) {
  490. for ( let i = 0; i < this._ranges.length; i++ ) {
  491. if ( range.isIntersecting( this._ranges[ i ] ) ) {
  492. /**
  493. * Trying to add a range that intersects with another range from selection.
  494. *
  495. * @error selection-range-intersects
  496. * @param {engine.model.Range} addedRange Range that was added to the selection.
  497. * @param {engine.model.Range} intersectingRange Range from selection that intersects with `addedRange`.
  498. */
  499. throw new CKEditorError(
  500. 'model-selection-range-intersects: Trying to add a range that intersects with another range from selection.',
  501. { addedRange: range, intersectingRange: this._ranges[ i ] }
  502. );
  503. }
  504. }
  505. }
  506. /**
  507. * Removes most recently added range from the selection.
  508. *
  509. * @protected
  510. */
  511. _popRange() {
  512. this._ranges.pop();
  513. }
  514. /**
  515. * Deletes ranges from internal range array. Uses {@link engine.model.Selection#_popRange _popRange} to
  516. * ensure proper ranges removal.
  517. *
  518. * @private
  519. */
  520. _removeAllRanges() {
  521. while ( this._ranges.length > 0 ) {
  522. this._popRange();
  523. }
  524. }
  525. /**
  526. * Fired whenever selection ranges are changed.
  527. *
  528. * @event engine.model.Selection#change:range
  529. * @param {Boolean} directChange Specifies whether the range change was caused by direct usage of `Selection` API (`true`)
  530. * or by changes done to {@link engine.model.Document model document} using {@link engine.model.Batch Batch} API (`false`).
  531. */
  532. /**
  533. * Fired whenever selection attributes are changed.
  534. *
  535. * @event engine.model.Selection#change:attribute
  536. * @param {Boolean} directChange Specifies whether the attributes changed by direct usage of the Selection API (`true`)
  537. * or by changes done to the {@link engine.model.Document model document} using the {@link engine.model.Batch Batch} API (`false`).
  538. * @param {Array.<String>} attributeKeys Array containing keys of attributes that changed.
  539. */
  540. }
  541. mix( Selection, EmitterMixin );