documentselection.js 39 KB

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