documentselection.js 34 KB

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