selection.js 23 KB

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