8
0

documentselection.js 39 KB

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