documentselection.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/documentselection
  7. */
  8. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  9. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  10. import Selection from './selection';
  11. import LiveRange from './liverange';
  12. import Text from './text';
  13. import TextProxy from './textproxy';
  14. import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
  15. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  16. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  17. import log from '@ckeditor/ckeditor5-utils/src/log';
  18. import uid from '@ckeditor/ckeditor5-utils/src/uid';
  19. const storePrefix = 'selection:';
  20. /**
  21. * `DocumentSelection` is a special selection which is used as the
  22. * {@link module:engine/model/document~Document#selection document's selection}.
  23. * There can be only one instance of `DocumentSelection` per document.
  24. *
  25. * Document selection can only be changed by using the {@link module:engine/model/writer~Writer} instance
  26. * inside the {@link module:engine/model/model~Model#change `change()`} block, as it provides a secure way to modify model.
  27. *
  28. * `DocumentSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
  29. * to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
  30. *
  31. * Differences between {@link module:engine/model/selection~Selection} and `DocumentSelection` are:
  32. * * there is always a range in `DocumentSelection` - even if no ranges were added there is a "default range"
  33. * present in the selection,
  34. * * ranges added to this selection updates automatically when the document changes,
  35. * * attributes of `DocumentSelection` are updated automatically according to selection ranges.
  36. *
  37. * Since `DocumentSelection` uses {@link module:engine/model/liverange~LiveRange live ranges}
  38. * and is updated when {@link module:engine/model/document~Document document}
  39. * changes, it cannot be set on {@link module:engine/model/node~Node nodes}
  40. * that are inside {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
  41. * If you need to represent a selection in document fragment,
  42. * use {@link module:engine/model/selection~Selection Selection class} instead.
  43. *
  44. * @mixes module:utils/emittermixin~EmitterMixin
  45. */
  46. export default class DocumentSelection {
  47. /**
  48. * Creates an empty live selection for given {@link module:engine/model/document~Document}.
  49. *
  50. * @param {module:engine/model/document~Document} doc Document which owns this selection.
  51. */
  52. constructor( doc ) {
  53. /**
  54. * Selection used internally by that class (`DocumentSelection` is a proxy to that selection).
  55. *
  56. * @protected
  57. */
  58. this._selection = new LiveSelection( doc );
  59. this._selection.delegate( 'change:range' ).to( this );
  60. this._selection.delegate( 'change:attribute' ).to( this );
  61. }
  62. /**
  63. * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
  64. * collapsed.
  65. *
  66. * @readonly
  67. * @type {Boolean}
  68. */
  69. get isCollapsed() {
  70. return this._selection.isCollapsed;
  71. }
  72. /**
  73. * Selection anchor. Anchor may be described as a position where the most recent part of the selection starts.
  74. * Together with {@link #focus} they define the direction of selection, which is important
  75. * when expanding/shrinking selection. Anchor is always {@link module:engine/model/range~Range#start start} or
  76. * {@link module:engine/model/range~Range#end end} position of the most recently added range.
  77. *
  78. * Is set to `null` if there are no ranges in selection.
  79. *
  80. * @see #focus
  81. * @readonly
  82. * @type {module:engine/model/position~Position|null}
  83. */
  84. get anchor() {
  85. return this._selection.anchor;
  86. }
  87. /**
  88. * Selection focus. Focus is a position where the selection ends.
  89. *
  90. * Is set to `null` if there are no ranges in selection.
  91. *
  92. * @see #anchor
  93. * @readonly
  94. * @type {module:engine/model/position~Position|null}
  95. */
  96. get focus() {
  97. return this._selection.focus;
  98. }
  99. /**
  100. * Returns number of ranges in selection.
  101. *
  102. * @readonly
  103. * @type {Number}
  104. */
  105. get rangeCount() {
  106. return this._selection.rangeCount;
  107. }
  108. /**
  109. * Describes whether `Documentselection` has own range(s) set, or if it is defaulted to
  110. * {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
  111. *
  112. * @readonly
  113. * @type {Boolean}
  114. */
  115. get hasOwnRange() {
  116. return this._selection.hasOwnRange;
  117. }
  118. /**
  119. * Specifies whether the {@link #focus}
  120. * precedes {@link #anchor}.
  121. *
  122. * @readonly
  123. * @type {Boolean}
  124. */
  125. get isBackward() {
  126. return this._selection.isBackward;
  127. }
  128. /**
  129. * Describes whether the gravity is overridden (using {@link module:engine/model/writer~Writer#overrideSelectionGravity}) or not.
  130. *
  131. * Note that the gravity remains overridden as long as will not be restored the same number of times as it was overridden.
  132. *
  133. * @readonly
  134. * @returns {Boolean}
  135. */
  136. get isGravityOverridden() {
  137. return this._selection.isGravityOverridden;
  138. }
  139. /**
  140. * A collection of selection markers.
  141. * Marker is a selection marker when selection range is inside the marker range.
  142. *
  143. * @readonly
  144. * @type {module:utils/collection~Collection.<module:engine/model/markercollection~Marker>}
  145. */
  146. get markers() {
  147. return this._selection.markers;
  148. }
  149. /**
  150. * Used for the compatibility with the {@link module:engine/model/selection~Selection#isEqual} method.
  151. *
  152. * @protected
  153. */
  154. get _ranges() {
  155. return this._selection._ranges;
  156. }
  157. /**
  158. * Returns an iterable that iterates over copies of selection ranges.
  159. *
  160. * @returns {Iterable.<module:engine/model/range~Range>}
  161. */
  162. getRanges() {
  163. return this._selection.getRanges();
  164. }
  165. /**
  166. * Returns the first position in the selection.
  167. * First position is the position that {@link module:engine/model/position~Position#isBefore is before}
  168. * any other position in the selection.
  169. *
  170. * Returns `null` if there are no ranges in selection.
  171. *
  172. * @returns {module:engine/model/position~Position|null}
  173. */
  174. getFirstPosition() {
  175. return this._selection.getFirstPosition();
  176. }
  177. /**
  178. * Returns the last position in the selection.
  179. * Last position is the position that {@link module:engine/model/position~Position#isAfter is after}
  180. * any other position in the selection.
  181. *
  182. * Returns `null` if there are no ranges in selection.
  183. *
  184. * @returns {module:engine/model/position~Position|null}
  185. */
  186. getLastPosition() {
  187. return this._selection.getLastPosition();
  188. }
  189. /**
  190. * Returns a copy of the first range in the selection.
  191. * First range is the one which {@link module:engine/model/range~Range#start start} position
  192. * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
  193. * (not to confuse with the first range added to the selection).
  194. *
  195. * Returns `null` if there are no ranges in selection.
  196. *
  197. * @returns {module:engine/model/range~Range|null}
  198. */
  199. getFirstRange() {
  200. return this._selection.getFirstRange();
  201. }
  202. /**
  203. * Returns a copy of the last range in the selection.
  204. * Last range is the one which {@link module:engine/model/range~Range#end end} position
  205. * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
  206. * recently added to the selection).
  207. *
  208. * Returns `null` if there are no ranges in selection.
  209. *
  210. * @returns {module:engine/model/range~Range|null}
  211. */
  212. getLastRange() {
  213. return this._selection.getLastRange();
  214. }
  215. /**
  216. * Gets elements of type "block" touched by the selection.
  217. *
  218. * This method's result can be used for example to apply block styling to all blocks covered by this selection.
  219. *
  220. * **Note:** `getSelectedBlocks()` always returns the deepest block.
  221. *
  222. * In this case the function will return exactly all 3 paragraphs:
  223. *
  224. * <paragraph>[a</paragraph>
  225. * <quote>
  226. * <paragraph>b</paragraph>
  227. * </quote>
  228. * <paragraph>c]d</paragraph>
  229. *
  230. * In this case the paragraph will also be returned, despite the collapsed selection:
  231. *
  232. * <paragraph>[]a</paragraph>
  233. *
  234. * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
  235. * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
  236. *
  237. * <paragraph>[a</paragraph>
  238. * <paragraph>b</paragraph>
  239. * <paragraph>]c</paragraph> // this block will not be returned
  240. *
  241. * @returns {Iterable.<module:engine/model/element~Element>}
  242. */
  243. getSelectedBlocks() {
  244. return this._selection.getSelectedBlocks();
  245. }
  246. /**
  247. * Returns blocks that aren't nested in other selected blocks.
  248. *
  249. * In this case the method will return blocks A, B and E because C & D are children of block B:
  250. *
  251. * [<blockA></blockA>
  252. * <blockB>
  253. * <blockC></blockC>
  254. * <blockD></blockD>
  255. * </blockB>
  256. * <blockE></blockE>]
  257. *
  258. * **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
  259. *
  260. * @returns {Iterable.<module:engine/model/element~Element>}
  261. */
  262. getTopMostBlocks() {
  263. return this._selection.getTopMostBlocks();
  264. }
  265. /**
  266. * Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
  267. * one range in the selection, and that range contains exactly one element.
  268. * Returns `null` if there is no selected element.
  269. *
  270. * @returns {module:engine/model/element~Element|null}
  271. */
  272. getSelectedElement() {
  273. return this._selection.getSelectedElement();
  274. }
  275. /**
  276. * Checks whether the selection contains the entire content of the given element. This means that selection must start
  277. * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
  278. * touching the element's end.
  279. *
  280. * By default, this method will check whether the entire content of the selection's current root is selected.
  281. * Useful to check if e.g. the user has just pressed <kbd>Ctrl</kbd> + <kbd>A</kbd>.
  282. *
  283. * @param {module:engine/model/element~Element} [element=this.anchor.root]
  284. * @returns {Boolean}
  285. */
  286. containsEntireContent( element ) {
  287. return this._selection.containsEntireContent( element );
  288. }
  289. /**
  290. * Unbinds all events previously bound by document selection.
  291. */
  292. destroy() {
  293. this._selection.destroy();
  294. }
  295. /**
  296. * Returns iterable that iterates over this selection's attribute keys.
  297. *
  298. * @returns {Iterable.<String>}
  299. */
  300. getAttributeKeys() {
  301. return this._selection.getAttributeKeys();
  302. }
  303. /**
  304. * Returns iterable that iterates over this selection's attributes.
  305. *
  306. * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
  307. * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
  308. *
  309. * @returns {Iterable.<*>}
  310. */
  311. getAttributes() {
  312. return this._selection.getAttributes();
  313. }
  314. /**
  315. * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
  316. *
  317. * @param {String} key Key of attribute to look for.
  318. * @returns {*} Attribute value or `undefined`.
  319. */
  320. getAttribute( key ) {
  321. return this._selection.getAttribute( key );
  322. }
  323. /**
  324. * Checks if the selection has an attribute for given key.
  325. *
  326. * @param {String} key Key of attribute to check.
  327. * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
  328. */
  329. hasAttribute( key ) {
  330. return this._selection.hasAttribute( key );
  331. }
  332. /**
  333. * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
  334. * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionFocus} method.
  335. *
  336. * The location can be specified in the same form as
  337. * {@link module:engine/model/writer~Writer#createPositionAt writer.createPositionAt()} parameters.
  338. *
  339. * @see module:engine/model/writer~Writer#setSelectionFocus
  340. * @protected
  341. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  342. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  343. * first parameter is a {@link module:engine/model/item~Item model item}.
  344. */
  345. _setFocus( itemOrPosition, offset ) {
  346. this._selection.setFocus( itemOrPosition, offset );
  347. }
  348. /**
  349. * Sets this selection's ranges and direction to the specified location based on the given
  350. * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
  351. * {@link module:engine/model/node~Node node}, {@link module:engine/model/position~Position position},
  352. * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
  353. * Should be used only within the {@link module:engine/model/writer~Writer#setSelection} method.
  354. *
  355. * @see module:engine/model/writer~Writer#setSelection
  356. * @protected
  357. * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
  358. * module:engine/model/position~Position|module:engine/model/node~Node|
  359. * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
  360. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
  361. * @param {Object} [options]
  362. * @param {Boolean} [options.backward] Sets this selection instance to be backward.
  363. */
  364. _setTo( selectable, placeOrOffset, options ) {
  365. this._selection.setTo( selectable, placeOrOffset, options );
  366. }
  367. /**
  368. * Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
  369. * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionAttribute} method.
  370. *
  371. * @see module:engine/model/writer~Writer#setSelectionAttribute
  372. * @protected
  373. * @param {String} key Key of the attribute to set.
  374. * @param {*} value Attribute value.
  375. */
  376. _setAttribute( key, value ) {
  377. this._selection.setAttribute( key, value );
  378. }
  379. /**
  380. * Removes an attribute with given key from the selection.
  381. * If the given attribute was set on the selection, fires the {@link module:engine/model/selection~Selection#event:change:range}
  382. * event with removed attribute key.
  383. * Should be used only within the {@link module:engine/model/writer~Writer#removeSelectionAttribute} method.
  384. *
  385. * @see module:engine/model/writer~Writer#removeSelectionAttribute
  386. * @protected
  387. * @param {String} key Key of the attribute to remove.
  388. */
  389. _removeAttribute( key ) {
  390. this._selection.removeAttribute( key );
  391. }
  392. /**
  393. * Returns an iterable that iterates through all selection attributes stored in current selection's parent.
  394. *
  395. * @protected
  396. * @returns {Iterable.<*>}
  397. */
  398. _getStoredAttributes() {
  399. return this._selection._getStoredAttributes();
  400. }
  401. /**
  402. * Temporarily changes the gravity of the selection from the left to the right.
  403. *
  404. * The gravity defines from which direction the selection inherits its attributes. If it's the default left
  405. * gravity, the selection (after being moved by the the user) inherits attributes from its left hand side.
  406. * This method allows to temporarily override this behavior by forcing the gravity to the right.
  407. *
  408. * It returns an unique identifier which is required to restore the gravity. It guarantees the symmetry
  409. * of the process.
  410. *
  411. * @see module:engine/model/writer~Writer#overrideSelectionGravity
  412. * @protected
  413. * @returns {String} The unique id which allows restoring the gravity.
  414. */
  415. _overrideGravity() {
  416. return this._selection.overrideGravity();
  417. }
  418. /**
  419. * Restores the {@link ~DocumentSelection#_overrideGravity overridden gravity}.
  420. *
  421. * Restoring the gravity is only possible using the unique identifier returned by
  422. * {@link ~DocumentSelection#_overrideGravity}. Note that the gravity remains overridden as long as won't be restored
  423. * the same number of times it was overridden.
  424. *
  425. * @see module:engine/model/writer~Writer#restoreSelectionGravity
  426. * @protected
  427. * @param {String} uid The unique id returned by {@link #_overrideGravity}.
  428. */
  429. _restoreGravity( uid ) {
  430. this._selection.restoreGravity( uid );
  431. }
  432. /**
  433. * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
  434. *
  435. * @protected
  436. * @param {String} key Attribute key to convert.
  437. * @returns {String} Converted attribute key, applicable for selection store.
  438. */
  439. static _getStoreAttributeKey( key ) {
  440. return storePrefix + key;
  441. }
  442. /**
  443. * Checks whether the given attribute key is an attribute stored on an element.
  444. *
  445. * @protected
  446. * @param {String} key
  447. * @returns {Boolean}
  448. */
  449. static _isStoreAttributeKey( key ) {
  450. return key.startsWith( storePrefix );
  451. }
  452. }
  453. mix( DocumentSelection, EmitterMixin );
  454. /**
  455. * Fired when selection range(s) changed.
  456. *
  457. * @event change:range
  458. * @param {Boolean} directChange In case of {@link module:engine/model/selection~Selection} class it is always set
  459. * to `true` which indicates that the selection change was caused by a direct use of selection's API.
  460. * The {@link module:engine/model/documentselection~DocumentSelection}, however, may change because its position
  461. * was directly changed through the {@link module:engine/model/writer~Writer writer} or because its position was
  462. * changed because the structure of the model has been changed (which means an indirect change).
  463. * The indirect change does not occur in case of normal (detached) selections because they are "static" (as "not live")
  464. * which mean that they are not updated once the document changes.
  465. */
  466. /**
  467. * Fired when selection attribute changed.
  468. *
  469. * @event change:attribute
  470. * @param {Boolean} directChange In case of {@link module:engine/model/selection~Selection} class it is always set
  471. * to `true` which indicates that the selection change was caused by a direct use of selection's API.
  472. * The {@link module:engine/model/documentselection~DocumentSelection}, however, may change because its attributes
  473. * were directly changed through the {@link module:engine/model/writer~Writer writer} or because its position was
  474. * changed in the model and its attributes were refreshed (which means an indirect change).
  475. * The indirect change does not occur in case of normal (detached) selections because they are "static" (as "not live")
  476. * which mean that they are not updated once the document changes.
  477. * @param {Array.<String>} attributeKeys Array containing keys of attributes that changed.
  478. */
  479. // `LiveSelection` is used internally by {@link module:engine/model/documentselection~DocumentSelection} and shouldn't be used directly.
  480. //
  481. // LiveSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
  482. // to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
  483. //
  484. // Differences between {@link module:engine/model/selection~Selection} and `LiveSelection` are:
  485. // * there is always a range in `LiveSelection` - even if no ranges were added there is a "default range"
  486. // present in the selection,
  487. // * ranges added to this selection updates automatically when the document changes,
  488. // * attributes of `LiveSelection` are updated automatically according to selection ranges.
  489. //
  490. // @extends module:engine/model/selection~Selection
  491. //
  492. class LiveSelection extends Selection {
  493. // Creates an empty live selection for given {@link module:engine/model/document~Document}.
  494. // @param {module:engine/model/document~Document} doc Document which owns this selection.
  495. constructor( doc ) {
  496. super();
  497. // List of selection markers.
  498. // Marker is a selection marker when selection range is inside the marker range.
  499. //
  500. // @type {module:utils/collection~Collection}
  501. this.markers = new Collection( { idProperty: 'name' } );
  502. // Document which owns this selection.
  503. //
  504. // @protected
  505. // @member {module:engine/model/model~Model}
  506. this._model = doc.model;
  507. // Document which owns this selection.
  508. //
  509. // @protected
  510. // @member {module:engine/model/document~Document}
  511. this._document = doc;
  512. // Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
  513. // last time. Possible values of priority are: `'low'` and `'normal'`.
  514. //
  515. // Priorities are used by internal `LiveSelection` mechanisms. All attributes set using `LiveSelection`
  516. // attributes API are set with `'normal'` priority.
  517. //
  518. // @private
  519. // @member {Map} module:engine/model/liveselection~LiveSelection#_attributePriority
  520. this._attributePriority = new Map();
  521. // Contains data required to fix ranges which have been moved to the graveyard.
  522. // @private
  523. // @member {Array} module:engine/model/liveselection~LiveSelection#_fixGraveyardRangesData
  524. this._fixGraveyardRangesData = [];
  525. // Flag that informs whether the selection ranges have changed. It is changed on true when `LiveRange#change:range` event is fired.
  526. // @private
  527. // @member {Array} module:engine/model/liveselection~LiveSelection#_hasChangedRange
  528. this._hasChangedRange = false;
  529. // Each overriding gravity adds an UID to the set and each removal removes it.
  530. // Gravity is overridden when there's at least one UID in the set.
  531. // Gravity is restored when the set is empty.
  532. // This is to prevent conflicts when gravity is overridden by more than one feature at the same time.
  533. // @private
  534. // @type {Set}
  535. this._overriddenGravityRegister = new Set();
  536. // Add events that will ensure selection correctness.
  537. this.on( 'change:range', () => {
  538. for ( const range of this.getRanges() ) {
  539. if ( !this._document._validateSelectionRange( range ) ) {
  540. /**
  541. * Range from {@link module:engine/model/documentselection~DocumentSelection document selection}
  542. * starts or ends at incorrect position.
  543. *
  544. * @error document-selection-wrong-position
  545. * @param {module:engine/model/range~Range} range
  546. */
  547. throw new CKEditorError(
  548. 'document-selection-wrong-position: Range from document selection starts or ends at incorrect position.',
  549. { range }
  550. );
  551. }
  552. }
  553. } );
  554. this.listenTo( this._document, 'change', ( evt, batch ) => {
  555. // Update selection's markers.
  556. this._updateMarkers();
  557. // Update selection's attributes.
  558. this._updateAttributes( false );
  559. // Clear selection attributes from element if no longer empty.
  560. clearAttributesStoredInElement( this._model, batch );
  561. } );
  562. this.listenTo( this._model, 'applyOperation', () => {
  563. while ( this._fixGraveyardRangesData.length ) {
  564. const { liveRange, sourcePosition } = this._fixGraveyardRangesData.shift();
  565. this._fixGraveyardSelection( liveRange, sourcePosition );
  566. }
  567. if ( this._hasChangedRange ) {
  568. this._hasChangedRange = false;
  569. this.fire( 'change:range', { directChange: false } );
  570. }
  571. }, { priority: 'lowest' } );
  572. }
  573. get isCollapsed() {
  574. const length = this._ranges.length;
  575. return length === 0 ? this._document._getDefaultRange().isCollapsed : super.isCollapsed;
  576. }
  577. get anchor() {
  578. return super.anchor || this._document._getDefaultRange().start;
  579. }
  580. get focus() {
  581. return super.focus || this._document._getDefaultRange().end;
  582. }
  583. get rangeCount() {
  584. return this._ranges.length ? this._ranges.length : 1;
  585. }
  586. // Describes whether `LiveSelection` has own range(s) set, or if it is defaulted to
  587. // {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
  588. //
  589. // @readonly
  590. // @type {Boolean}
  591. get hasOwnRange() {
  592. return this._ranges.length > 0;
  593. }
  594. // When set to `true` then selection attributes on node before the caret won't be taken
  595. // into consideration while updating selection attributes.
  596. //
  597. // @protected
  598. // @type {Boolean}
  599. get isGravityOverridden() {
  600. return !!this._overriddenGravityRegister.size;
  601. }
  602. // Unbinds all events previously bound by live selection.
  603. destroy() {
  604. for ( let i = 0; i < this._ranges.length; i++ ) {
  605. this._ranges[ i ].detach();
  606. }
  607. this.stopListening();
  608. }
  609. * getRanges() {
  610. if ( this._ranges.length ) {
  611. yield* super.getRanges();
  612. } else {
  613. yield this._document._getDefaultRange();
  614. }
  615. }
  616. getFirstRange() {
  617. return super.getFirstRange() || this._document._getDefaultRange();
  618. }
  619. getLastRange() {
  620. return super.getLastRange() || this._document._getDefaultRange();
  621. }
  622. setTo( selectable, optionsOrPlaceOrOffset, options ) {
  623. super.setTo( selectable, optionsOrPlaceOrOffset, options );
  624. this._refreshAttributes();
  625. }
  626. setFocus( itemOrPosition, offset ) {
  627. super.setFocus( itemOrPosition, offset );
  628. this._refreshAttributes();
  629. }
  630. setAttribute( key, value ) {
  631. if ( this._setAttribute( key, value ) ) {
  632. // Fire event with exact data.
  633. const attributeKeys = [ key ];
  634. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  635. }
  636. }
  637. removeAttribute( key ) {
  638. if ( this._removeAttribute( key ) ) {
  639. // Fire event with exact data.
  640. const attributeKeys = [ key ];
  641. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  642. }
  643. }
  644. overrideGravity() {
  645. const overrideUid = uid();
  646. // Remember that another overriding has been requested. It will need to be removed
  647. // before the gravity is to be restored.
  648. this._overriddenGravityRegister.add( overrideUid );
  649. if ( this._overriddenGravityRegister.size === 1 ) {
  650. this._refreshAttributes();
  651. }
  652. return overrideUid;
  653. }
  654. restoreGravity( uid ) {
  655. if ( !this._overriddenGravityRegister.has( uid ) ) {
  656. /**
  657. * Restoring gravity for an unknown UID is not possible. Make sure you are using a correct
  658. * UID obtained from the {@link module:engine/model/writer~Writer#overrideSelectionGravity} to restore.
  659. *
  660. * @error document-selection-gravity-wrong-restore
  661. * @param {String} uid The unique identifier returned by
  662. * {@link module:engine/model/documentselection~DocumentSelection#_overrideGravity}.
  663. */
  664. throw new CKEditorError(
  665. 'document-selection-gravity-wrong-restore: Attempting to restore the selection gravity for an unknown UID.',
  666. { uid }
  667. );
  668. }
  669. this._overriddenGravityRegister.delete( uid );
  670. // Restore gravity only when all overriding have been restored.
  671. if ( !this.isGravityOverridden ) {
  672. this._refreshAttributes();
  673. }
  674. }
  675. // Removes all attributes from the selection and sets attributes according to the surrounding nodes.
  676. _refreshAttributes() {
  677. this._updateAttributes( true );
  678. }
  679. _popRange() {
  680. this._ranges.pop().detach();
  681. }
  682. _pushRange( range ) {
  683. const liveRange = this._prepareRange( range );
  684. // `undefined` is returned when given `range` is in graveyard root.
  685. if ( liveRange ) {
  686. this._ranges.push( liveRange );
  687. }
  688. }
  689. // Prepares given range to be added to selection. Checks if it is correct,
  690. // converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
  691. // and sets listeners listening to the range's change event.
  692. //
  693. // @private
  694. // @param {module:engine/model/range~Range} range
  695. _prepareRange( range ) {
  696. this._checkRange( range );
  697. if ( range.root == this._document.graveyard ) {
  698. /**
  699. * Trying to add a Range that is in the graveyard root. Range rejected.
  700. *
  701. * @warning model-selection-range-in-graveyard
  702. */
  703. log.warn( 'model-selection-range-in-graveyard: Trying to add a Range that is in the graveyard root. Range rejected.' );
  704. return;
  705. }
  706. const liveRange = LiveRange.fromRange( range );
  707. liveRange.on( 'change:range', ( evt, oldRange, data ) => {
  708. this._hasChangedRange = true;
  709. // If `LiveRange` is in whole moved to the graveyard, save necessary data. It will be fixed on `Model#applyOperation` event.
  710. if ( liveRange.root == this._document.graveyard ) {
  711. this._fixGraveyardRangesData.push( {
  712. liveRange,
  713. sourcePosition: data.deletionPosition
  714. } );
  715. }
  716. } );
  717. return liveRange;
  718. }
  719. _updateMarkers() {
  720. const markers = [];
  721. for ( const marker of this._model.markers ) {
  722. const markerRange = marker.getRange();
  723. for ( const selectionRange of this.getRanges() ) {
  724. if ( markerRange.containsRange( selectionRange, !selectionRange.isCollapsed ) ) {
  725. markers.push( marker );
  726. }
  727. }
  728. }
  729. for ( const marker of markers ) {
  730. if ( this.markers.getIndex( marker ) < 0 ) {
  731. this.markers.add( marker );
  732. }
  733. }
  734. for ( const marker of Array.from( this.markers ) ) {
  735. if ( !markers.includes( marker ) ) {
  736. this.markers.remove( marker );
  737. }
  738. }
  739. }
  740. // Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
  741. //
  742. // @protected
  743. // @param {Boolean} clearAll
  744. // @fires change:attribute
  745. _updateAttributes( clearAll ) {
  746. const newAttributes = toMap( this._getSurroundingAttributes() );
  747. const oldAttributes = toMap( this.getAttributes() );
  748. if ( clearAll ) {
  749. // If `clearAll` remove all attributes and reset priorities.
  750. this._attributePriority = new Map();
  751. this._attrs = new Map();
  752. } else {
  753. // If not, remove only attributes added with `low` priority.
  754. for ( const [ key, priority ] of this._attributePriority ) {
  755. if ( priority == 'low' ) {
  756. this._attrs.delete( key );
  757. this._attributePriority.delete( key );
  758. }
  759. }
  760. }
  761. this._setAttributesTo( newAttributes );
  762. // Let's evaluate which attributes really changed.
  763. const changed = [];
  764. // First, loop through all attributes that are set on selection right now.
  765. // Check which of them are different than old attributes.
  766. for ( const [ newKey, newValue ] of this.getAttributes() ) {
  767. if ( !oldAttributes.has( newKey ) || oldAttributes.get( newKey ) !== newValue ) {
  768. changed.push( newKey );
  769. }
  770. }
  771. // Then, check which of old attributes got removed.
  772. for ( const [ oldKey ] of oldAttributes ) {
  773. if ( !this.hasAttribute( oldKey ) ) {
  774. changed.push( oldKey );
  775. }
  776. }
  777. // Fire event with exact data (fire only if anything changed).
  778. if ( changed.length > 0 ) {
  779. this.fire( 'change:attribute', { attributeKeys: changed, directChange: false } );
  780. }
  781. }
  782. // Internal method for setting `LiveSelection` attribute. Supports attribute priorities (through `directChange`
  783. // parameter).
  784. //
  785. // @private
  786. // @param {String} key Attribute key.
  787. // @param {*} value Attribute value.
  788. // @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
  789. // is caused by `Batch` API.
  790. // @returns {Boolean} Whether value has changed.
  791. _setAttribute( key, value, directChange = true ) {
  792. const priority = directChange ? 'normal' : 'low';
  793. if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
  794. // Priority too low.
  795. return false;
  796. }
  797. const oldValue = super.getAttribute( key );
  798. // Don't do anything if value has not changed.
  799. if ( oldValue === value ) {
  800. return false;
  801. }
  802. this._attrs.set( key, value );
  803. // Update priorities map.
  804. this._attributePriority.set( key, priority );
  805. return true;
  806. }
  807. // Internal method for removing `LiveSelection` attribute. Supports attribute priorities (through `directChange`
  808. // parameter).
  809. //
  810. // NOTE: Even if attribute is not present in the selection but is provided to this method, it's priority will
  811. // be changed according to `directChange` parameter.
  812. //
  813. // @private
  814. // @param {String} key Attribute key.
  815. // @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
  816. // is caused by `Batch` API.
  817. // @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
  818. // existing attribute had higher priority.
  819. _removeAttribute( key, directChange = true ) {
  820. const priority = directChange ? 'normal' : 'low';
  821. if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
  822. // Priority too low.
  823. return false;
  824. }
  825. // Update priorities map.
  826. this._attributePriority.set( key, priority );
  827. // Don't do anything if value has not changed.
  828. if ( !super.hasAttribute( key ) ) {
  829. return false;
  830. }
  831. this._attrs.delete( key );
  832. return true;
  833. }
  834. // Internal method for setting multiple `LiveSelection` attributes. Supports attribute priorities (through
  835. // `directChange` parameter).
  836. //
  837. // @private
  838. // @param {Map.<String,*>} attrs Iterable object containing attributes to be set.
  839. // @returns {Set.<String>} Changed attribute keys.
  840. _setAttributesTo( attrs ) {
  841. const changed = new Set();
  842. for ( const [ oldKey, oldValue ] of this.getAttributes() ) {
  843. // Do not remove attribute if attribute with same key and value is about to be set.
  844. if ( attrs.get( oldKey ) === oldValue ) {
  845. continue;
  846. }
  847. // All rest attributes will be removed so changed attributes won't change .
  848. this._removeAttribute( oldKey, false );
  849. }
  850. for ( const [ key, value ] of attrs ) {
  851. // Attribute may not be set because of attributes or because same key/value is already added.
  852. const gotAdded = this._setAttribute( key, value, false );
  853. if ( gotAdded ) {
  854. changed.add( key );
  855. }
  856. }
  857. return changed;
  858. }
  859. // Returns an iterable that iterates through all selection attributes stored in current selection's parent.
  860. //
  861. // @protected
  862. // @returns {Iterable.<*>}
  863. * _getStoredAttributes() {
  864. const selectionParent = this.getFirstPosition().parent;
  865. if ( this.isCollapsed && selectionParent.isEmpty ) {
  866. for ( const key of selectionParent.getAttributeKeys() ) {
  867. if ( key.startsWith( storePrefix ) ) {
  868. const realKey = key.substr( storePrefix.length );
  869. yield [ realKey, selectionParent.getAttribute( key ) ];
  870. }
  871. }
  872. }
  873. }
  874. // Checks model text nodes that are closest to the selection's first position and returns attributes of first
  875. // found element. If there are no text nodes in selection's first position parent, it returns selection
  876. // attributes stored in that parent.
  877. //
  878. // @private
  879. // @returns {Iterable.<*>} Collection of attributes.
  880. _getSurroundingAttributes() {
  881. const position = this.getFirstPosition();
  882. const schema = this._model.schema;
  883. let attrs = null;
  884. if ( !this.isCollapsed ) {
  885. // 1. If selection is a range...
  886. const range = this.getFirstRange();
  887. // ...look for a first character node in that range and take attributes from it.
  888. for ( const value of range ) {
  889. // If the item is an object, we don't want to get attributes from its children.
  890. if ( value.item.is( 'element' ) && schema.isObject( value.item ) ) {
  891. break;
  892. }
  893. // This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
  894. // It can be done better by using `break;` instead of checking `attrs === null`.
  895. if ( value.type == 'text' && attrs === null ) {
  896. attrs = value.item.getAttributes();
  897. }
  898. }
  899. } else {
  900. // 2. If the selection is a caret or the range does not contain a character node...
  901. const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
  902. const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
  903. // When gravity is overridden then don't take node before into consideration.
  904. if ( !this.isGravityOverridden ) {
  905. // ...look at the node before caret and take attributes from it if it is a character node.
  906. attrs = getAttrsIfCharacter( nodeBefore );
  907. }
  908. // 3. If not, look at the node after caret...
  909. if ( !attrs ) {
  910. attrs = getAttrsIfCharacter( nodeAfter );
  911. }
  912. // 4. If not, try to find the first character on the left, that is in the same node.
  913. // When gravity is overridden then don't take node before into consideration.
  914. if ( !this.isGravityOverridden && !attrs ) {
  915. let node = nodeBefore;
  916. while ( node && !attrs ) {
  917. node = node.previousSibling;
  918. attrs = getAttrsIfCharacter( node );
  919. }
  920. }
  921. // 5. If not found, try to find the first character on the right, that is in the same node.
  922. if ( !attrs ) {
  923. let node = nodeAfter;
  924. while ( node && !attrs ) {
  925. node = node.nextSibling;
  926. attrs = getAttrsIfCharacter( node );
  927. }
  928. }
  929. // 6. If not found, selection should retrieve attributes from parent.
  930. if ( !attrs ) {
  931. attrs = this._getStoredAttributes();
  932. }
  933. }
  934. return attrs;
  935. }
  936. // Fixes a selection range after it ends up in graveyard root.
  937. //
  938. // @private
  939. // @param {module:engine/model/liverange~LiveRange} liveRange The range from selection, that ended up in the graveyard root.
  940. // @param {module:engine/model/position~Position} removedRangeStart Start position of a range which was removed.
  941. _fixGraveyardSelection( liveRange, removedRangeStart ) {
  942. // The start of the removed range is the closest position to the `liveRange` - the original selection range.
  943. // This is a good candidate for a fixed selection range.
  944. const positionCandidate = removedRangeStart.clone();
  945. // Find a range that is a correct selection range and is closest to the start of removed range.
  946. const selectionRange = this._model.schema.getNearestSelectionRange( positionCandidate );
  947. // Remove the old selection range before preparing and adding new selection range. This order is important,
  948. // because new range, in some cases, may intersect with old range (it depends on `getNearestSelectionRange()` result).
  949. const index = this._ranges.indexOf( liveRange );
  950. this._ranges.splice( index, 1 );
  951. liveRange.detach();
  952. // If nearest valid selection range has been found - add it in the place of old range.
  953. if ( selectionRange ) {
  954. // Check the range, convert it to live range, bind events, etc.
  955. const newRange = this._prepareRange( selectionRange );
  956. // Add new range in the place of old range.
  957. this._ranges.splice( index, 0, newRange );
  958. }
  959. // If nearest valid selection range cannot be found - just removing the old range is fine.
  960. }
  961. }
  962. // Helper function for {@link module:engine/model/liveselection~LiveSelection#_updateAttributes}.
  963. //
  964. // It takes model item, checks whether it is a text node (or text proxy) and, if so, returns it's attributes. If not, returns `null`.
  965. //
  966. // @param {module:engine/model/item~Item|null} node
  967. // @returns {Boolean}
  968. function getAttrsIfCharacter( node ) {
  969. if ( node instanceof TextProxy || node instanceof Text ) {
  970. return node.getAttributes();
  971. }
  972. return null;
  973. }
  974. // Removes selection attributes from element which is not empty anymore.
  975. //
  976. // @private
  977. // @param {module:engine/model/model~Model} model
  978. // @param {module:engine/model/batch~Batch} batch
  979. function clearAttributesStoredInElement( model, batch ) {
  980. const differ = model.document.differ;
  981. for ( const entry of differ.getChanges() ) {
  982. if ( entry.type != 'insert' ) {
  983. continue;
  984. }
  985. const changeParent = entry.position.parent;
  986. const isNoLongerEmpty = entry.length === changeParent.maxOffset;
  987. if ( isNoLongerEmpty ) {
  988. model.enqueueChange( batch, writer => {
  989. const storedAttributes = Array.from( changeParent.getAttributeKeys() )
  990. .filter( key => key.startsWith( storePrefix ) );
  991. for ( const key of storedAttributes ) {
  992. writer.removeAttribute( key, changeParent );
  993. }
  994. } );
  995. }
  996. }
  997. }