8
0

documentselection.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934
  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. * Used for the compatibility with the {@link module:engine/model/selection~Selection#isEqual} method.
  130. *
  131. * @protected
  132. */
  133. get _ranges() {
  134. return this._selection._ranges;
  135. }
  136. /**
  137. * Returns an iterable that iterates over copies of selection ranges.
  138. *
  139. * @returns {Iterable.<module:engine/model/range~Range>}
  140. */
  141. getRanges() {
  142. return this._selection.getRanges();
  143. }
  144. /**
  145. * Returns the first position in the selection.
  146. * First position is the position that {@link module:engine/model/position~Position#isBefore is before}
  147. * any other position in the selection.
  148. *
  149. * Returns `null` if there are no ranges in selection.
  150. *
  151. * @returns {module:engine/model/position~Position|null}
  152. */
  153. getFirstPosition() {
  154. return this._selection.getFirstPosition();
  155. }
  156. /**
  157. * Returns the last position in the selection.
  158. * Last position is the position that {@link module:engine/model/position~Position#isAfter is after}
  159. * any other position in the selection.
  160. *
  161. * Returns `null` if there are no ranges in selection.
  162. *
  163. * @returns {module:engine/model/position~Position|null}
  164. */
  165. getLastPosition() {
  166. return this._selection.getLastPosition();
  167. }
  168. /**
  169. * Returns a copy of the first range in the selection.
  170. * First range is the one which {@link module:engine/model/range~Range#start start} position
  171. * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
  172. * (not to confuse with the first range added to the selection).
  173. *
  174. * Returns `null` if there are no ranges in selection.
  175. *
  176. * @returns {module:engine/model/range~Range|null}
  177. */
  178. getFirstRange() {
  179. return this._selection.getFirstRange();
  180. }
  181. /**
  182. * Returns a copy of the last range in the selection.
  183. * Last range is the one which {@link module:engine/model/range~Range#end end} position
  184. * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
  185. * recently added to the selection).
  186. *
  187. * Returns `null` if there are no ranges in selection.
  188. *
  189. * @returns {module:engine/model/range~Range|null}
  190. */
  191. getLastRange() {
  192. return this._selection.getLastRange();
  193. }
  194. /**
  195. * Gets elements of type "block" touched by the selection.
  196. *
  197. * This method's result can be used for example to apply block styling to all blocks covered by this selection.
  198. *
  199. * **Note:** `getSelectedBlocks()` always returns the deepest block.
  200. *
  201. * In this case the function will return exactly all 3 paragraphs:
  202. *
  203. * <paragraph>[a</paragraph>
  204. * <quote>
  205. * <paragraph>b</paragraph>
  206. * </quote>
  207. * <paragraph>c]d</paragraph>
  208. *
  209. * In this case the paragraph will also be returned, despite the collapsed selection:
  210. *
  211. * <paragraph>[]a</paragraph>
  212. *
  213. * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
  214. * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
  215. *
  216. * <paragraph>[a</paragraph>
  217. * <paragraph>b</paragraph>
  218. * <paragraph>]c</paragraph> // this block will not be returned
  219. *
  220. * @returns {Iterator.<module:engine/model/element~Element>}
  221. */
  222. getSelectedBlocks() {
  223. return this._selection.getSelectedBlocks();
  224. }
  225. /**
  226. * Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
  227. * one range in the selection, and that range contains exactly one element.
  228. * Returns `null` if there is no selected element.
  229. *
  230. * @returns {module:engine/model/element~Element|null}
  231. */
  232. getSelectedElement() {
  233. return this._selection.getSelectedElement();
  234. }
  235. /**
  236. * Checks whether the selection contains the entire content of the given element. This means that selection must start
  237. * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
  238. * touching the element's end.
  239. *
  240. * By default, this method will check whether the entire content of the selection's current root is selected.
  241. * Useful to check if e.g. the user has just pressed <kbd>Ctrl</kbd> + <kbd>A</kbd>.
  242. *
  243. * @param {module:engine/model/element~Element} [element=this.anchor.root]
  244. * @returns {Boolean}
  245. */
  246. containsEntireContent( element ) {
  247. return this._selection.containsEntireContent( element );
  248. }
  249. /**
  250. * Unbinds all events previously bound by document selection.
  251. */
  252. destroy() {
  253. this._selection.destroy();
  254. }
  255. /**
  256. * Returns iterable that iterates over this selection's attribute keys.
  257. *
  258. * @returns {Iterable.<String>}
  259. */
  260. getAttributeKeys() {
  261. return this._selection.getAttributeKeys();
  262. }
  263. /**
  264. * Returns iterable that iterates over this selection's attributes.
  265. *
  266. * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
  267. * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
  268. *
  269. * @returns {Iterable.<*>}
  270. */
  271. getAttributes() {
  272. return this._selection.getAttributes();
  273. }
  274. /**
  275. * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
  276. *
  277. * @param {String} key Key of attribute to look for.
  278. * @returns {*} Attribute value or `undefined`.
  279. */
  280. getAttribute( key ) {
  281. return this._selection.getAttribute( key );
  282. }
  283. /**
  284. * Checks if the selection has an attribute for given key.
  285. *
  286. * @param {String} key Key of attribute to check.
  287. * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
  288. */
  289. hasAttribute( key ) {
  290. return this._selection.hasAttribute( key );
  291. }
  292. /**
  293. * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
  294. * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionFocus} method.
  295. *
  296. * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
  297. *
  298. * @see module:engine/model/writer~Writer#setSelectionFocus
  299. * @protected
  300. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  301. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  302. * first parameter is a {@link module:engine/model/item~Item model item}.
  303. */
  304. _setFocus( itemOrPosition, offset ) {
  305. this._selection.setFocus( itemOrPosition, offset );
  306. }
  307. /**
  308. * Sets this selection's ranges and direction to the specified location based on the given
  309. * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
  310. * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
  311. * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
  312. * Should be used only within the {@link module:engine/model/writer~Writer#setSelection} method.
  313. *
  314. * @see module:engine/model/writer~Writer#setSelection
  315. * @protected
  316. * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
  317. * module:engine/model/position~Position|module:engine/model/element~Element|
  318. * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
  319. * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
  320. */
  321. _setTo( selectable, backwardSelectionOrOffset ) {
  322. this._selection.setTo( selectable, backwardSelectionOrOffset );
  323. }
  324. /**
  325. * Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
  326. * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionAttribute} method.
  327. *
  328. * @see module:engine/model/writer~Writer#setSelectionAttribute
  329. * @protected
  330. * @param {String} key Key of the attribute to set.
  331. * @param {*} value Attribute value.
  332. */
  333. _setAttribute( key, value ) {
  334. this._selection.setAttribute( key, value );
  335. }
  336. /**
  337. * Removes an attribute with given key from the selection.
  338. * If the given attribute was set on the selection, fires the {@link module:engine/model/selection~Selection#event:change}
  339. * event with removed attribute key.
  340. * Should be used only within the {@link module:engine/model/writer~Writer#removeSelectionAttribute} method.
  341. *
  342. * @see module:engine/model/writer~Writer#removeSelectionAttribute
  343. * @protected
  344. * @param {String} key Key of the attribute to remove.
  345. */
  346. _removeAttribute( key ) {
  347. this._selection.removeAttribute( key );
  348. }
  349. /**
  350. * Returns an iterable that iterates through all selection attributes stored in current selection's parent.
  351. *
  352. * @protected
  353. * @returns {Iterable.<*>}
  354. */
  355. _getStoredAttributes() {
  356. return this._selection._getStoredAttributes();
  357. }
  358. /**
  359. * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
  360. *
  361. * @protected
  362. * @param {String} key Attribute key to convert.
  363. * @returns {String} Converted attribute key, applicable for selection store.
  364. */
  365. static _getStoreAttributeKey( key ) {
  366. return storePrefix + key;
  367. }
  368. /**
  369. * Checks whether the given attribute key is an attribute stored on an element.
  370. *
  371. * @protected
  372. * @param {String} key
  373. * @returns {Boolean}
  374. */
  375. static _isStoreAttributeKey( key ) {
  376. return key.startsWith( storePrefix );
  377. }
  378. }
  379. mix( DocumentSelection, EmitterMixin );
  380. // `LiveSelection` is used internally by {@link module:engine/model/documentselection~DocumentSelection} and shouldn't be used directly.
  381. //
  382. // LiveSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
  383. // to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
  384. //
  385. // Differences between {@link module:engine/model/selection~Selection} and `LiveSelection` are:
  386. // * there is always a range in `LiveSelection` - even if no ranges were added there is a "default range"
  387. // present in the selection,
  388. // * ranges added to this selection updates automatically when the document changes,
  389. // * attributes of `LiveSelection` are updated automatically according to selection ranges.
  390. //
  391. // @extends module:engine/model/selection~Selection
  392. //
  393. class LiveSelection extends Selection {
  394. // Creates an empty live selection for given {@link module:engine/model/document~Document}.
  395. // @param {module:engine/model/document~Document} doc Document which owns this selection.
  396. constructor( doc ) {
  397. super();
  398. // Document which owns this selection.
  399. //
  400. // @protected
  401. // @member {module:engine/model/model~Model}
  402. this._model = doc.model;
  403. // Document which owns this selection.
  404. //
  405. // @protected
  406. // @member {module:engine/model/document~Document}
  407. this._document = doc;
  408. // Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
  409. // last time. Possible values of priority are: `'low'` and `'normal'`.
  410. //
  411. // Priorities are used by internal `LiveSelection` mechanisms. All attributes set using `LiveSelection`
  412. // attributes API are set with `'normal'` priority.
  413. //
  414. // @private
  415. // @member {Map} module:engine/model/liveselection~LiveSelection#_attributePriority
  416. this._attributePriority = new Map();
  417. // Add events that will ensure selection correctness.
  418. this.on( 'change:range', () => {
  419. for ( const range of this.getRanges() ) {
  420. if ( !this._document._validateSelectionRange( range ) ) {
  421. /**
  422. * Range from {@link module:engine/model/documentselection~DocumentSelection document selection}
  423. * starts or ends at incorrect position.
  424. *
  425. * @error document-selection-wrong-position
  426. * @param {module:engine/model/range~Range} range
  427. */
  428. throw new CKEditorError(
  429. 'document-selection-wrong-position: Range from document selection starts or ends at incorrect position.',
  430. { range }
  431. );
  432. }
  433. }
  434. } );
  435. this.listenTo( this._model, 'applyOperation', ( evt, args ) => {
  436. const operation = args[ 0 ];
  437. if ( !operation.isDocumentOperation ) {
  438. return;
  439. }
  440. // Whenever attribute operation is performed on document, update selection attributes.
  441. // This is not the most efficient way to update selection attributes, but should be okay for now.
  442. if ( attrOpTypes.has( operation.type ) ) {
  443. this._updateAttributes( false );
  444. }
  445. const batch = operation.delta.batch;
  446. // Batch may not be passed to the document#change event in some tests.
  447. // See https://github.com/ckeditor/ckeditor5-engine/issues/1001#issuecomment-314202352
  448. if ( batch ) {
  449. // Whenever element which had selection's attributes stored in it stops being empty,
  450. // the attributes need to be removed.
  451. clearAttributesStoredInElement( operation, this._model, batch );
  452. }
  453. }, { priority: 'low' } );
  454. }
  455. get isCollapsed() {
  456. const length = this._ranges.length;
  457. return length === 0 ? this._document._getDefaultRange().isCollapsed : super.isCollapsed;
  458. }
  459. get anchor() {
  460. return super.anchor || this._document._getDefaultRange().start;
  461. }
  462. get focus() {
  463. return super.focus || this._document._getDefaultRange().end;
  464. }
  465. get rangeCount() {
  466. return this._ranges.length ? this._ranges.length : 1;
  467. }
  468. // Describes whether `LiveSelection` has own range(s) set, or if it is defaulted to
  469. // {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
  470. //
  471. // @readonly
  472. // @type {Boolean}
  473. get hasOwnRange() {
  474. return this._ranges.length > 0;
  475. }
  476. // Unbinds all events previously bound by live selection.
  477. destroy() {
  478. for ( let i = 0; i < this._ranges.length; i++ ) {
  479. this._ranges[ i ].detach();
  480. }
  481. this.stopListening();
  482. }
  483. * getRanges() {
  484. if ( this._ranges.length ) {
  485. yield* super.getRanges();
  486. } else {
  487. yield this._document._getDefaultRange();
  488. }
  489. }
  490. getFirstRange() {
  491. return super.getFirstRange() || this._document._getDefaultRange();
  492. }
  493. getLastRange() {
  494. return super.getLastRange() || this._document._getDefaultRange();
  495. }
  496. setTo( selectable, backwardSelectionOrOffset ) {
  497. super.setTo( selectable, backwardSelectionOrOffset );
  498. this._refreshAttributes();
  499. }
  500. setFocus( itemOrPosition, offset ) {
  501. super.setFocus( itemOrPosition, offset );
  502. this._refreshAttributes();
  503. }
  504. setAttribute( key, value ) {
  505. if ( this._setAttribute( key, value ) ) {
  506. // Fire event with exact data.
  507. const attributeKeys = [ key ];
  508. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  509. }
  510. }
  511. removeAttribute( key ) {
  512. if ( this._removeAttribute( key ) ) {
  513. // Fire event with exact data.
  514. const attributeKeys = [ key ];
  515. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  516. }
  517. }
  518. // Removes all attributes from the selection and sets attributes according to the surrounding nodes.
  519. _refreshAttributes() {
  520. this._updateAttributes( true );
  521. }
  522. _popRange() {
  523. this._ranges.pop().detach();
  524. }
  525. _pushRange( range ) {
  526. const liveRange = this._prepareRange( range );
  527. // `undefined` is returned when given `range` is in graveyard root.
  528. if ( liveRange ) {
  529. this._ranges.push( liveRange );
  530. }
  531. }
  532. // Prepares given range to be added to selection. Checks if it is correct,
  533. // converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
  534. // and sets listeners listening to the range's change event.
  535. //
  536. // @private
  537. // @param {module:engine/model/range~Range} range
  538. _prepareRange( range ) {
  539. this._checkRange( range );
  540. if ( range.root == this._document.graveyard ) {
  541. /**
  542. * Trying to add a Range that is in the graveyard root. Range rejected.
  543. *
  544. * @warning model-selection-range-in-graveyard
  545. */
  546. log.warn( 'model-selection-range-in-graveyard: Trying to add a Range that is in the graveyard root. Range rejected.' );
  547. return;
  548. }
  549. const liveRange = LiveRange.createFromRange( range );
  550. liveRange.on( 'change:range', ( evt, oldRange, data ) => {
  551. // If `LiveRange` is in whole moved to the graveyard, fix that range.
  552. if ( liveRange.root == this._document.graveyard ) {
  553. this._fixGraveyardSelection( liveRange, data.sourcePosition );
  554. }
  555. // Whenever a live range from selection changes, fire an event informing about that change.
  556. this.fire( 'change:range', { directChange: false } );
  557. } );
  558. return liveRange;
  559. }
  560. // Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
  561. //
  562. // @protected
  563. // @param {Boolean} clearAll
  564. // @fires change:attribute
  565. _updateAttributes( clearAll ) {
  566. const newAttributes = toMap( this._getSurroundingAttributes() );
  567. const oldAttributes = toMap( this.getAttributes() );
  568. if ( clearAll ) {
  569. // If `clearAll` remove all attributes and reset priorities.
  570. this._attributePriority = new Map();
  571. this._attrs = new Map();
  572. } else {
  573. // If not, remove only attributes added with `low` priority.
  574. for ( const [ key, priority ] of this._attributePriority ) {
  575. if ( priority == 'low' ) {
  576. this._attrs.delete( key );
  577. this._attributePriority.delete( key );
  578. }
  579. }
  580. }
  581. this._setAttributesTo( newAttributes );
  582. // Let's evaluate which attributes really changed.
  583. const changed = [];
  584. // First, loop through all attributes that are set on selection right now.
  585. // Check which of them are different than old attributes.
  586. for ( const [ newKey, newValue ] of this.getAttributes() ) {
  587. if ( !oldAttributes.has( newKey ) || oldAttributes.get( newKey ) !== newValue ) {
  588. changed.push( newKey );
  589. }
  590. }
  591. // Then, check which of old attributes got removed.
  592. for ( const [ oldKey ] of oldAttributes ) {
  593. if ( !this.hasAttribute( oldKey ) ) {
  594. changed.push( oldKey );
  595. }
  596. }
  597. // Fire event with exact data (fire only if anything changed).
  598. if ( changed.length > 0 ) {
  599. this.fire( 'change:attribute', { attributeKeys: changed, directChange: false } );
  600. }
  601. }
  602. // Internal method for setting `LiveSelection` attribute. Supports attribute priorities (through `directChange`
  603. // parameter).
  604. //
  605. // @private
  606. // @param {String} key Attribute key.
  607. // @param {*} value Attribute value.
  608. // @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
  609. // is caused by `Batch` API.
  610. // @returns {Boolean} Whether value has changed.
  611. _setAttribute( key, value, directChange = true ) {
  612. const priority = directChange ? 'normal' : 'low';
  613. if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
  614. // Priority too low.
  615. return false;
  616. }
  617. const oldValue = super.getAttribute( key );
  618. // Don't do anything if value has not changed.
  619. if ( oldValue === value ) {
  620. return false;
  621. }
  622. this._attrs.set( key, value );
  623. // Update priorities map.
  624. this._attributePriority.set( key, priority );
  625. return true;
  626. }
  627. // Internal method for removing `LiveSelection` attribute. Supports attribute priorities (through `directChange`
  628. // parameter).
  629. //
  630. // @private
  631. // @param {String} key Attribute key.
  632. // @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
  633. // is caused by `Batch` API.
  634. // @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
  635. // existing attribute had higher priority.
  636. _removeAttribute( key, directChange = true ) {
  637. const priority = directChange ? 'normal' : 'low';
  638. if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
  639. // Priority too low.
  640. return false;
  641. }
  642. // Don't do anything if value has not changed.
  643. if ( !super.hasAttribute( key ) ) {
  644. return false;
  645. }
  646. this._attrs.delete( key );
  647. // Update priorities map.
  648. this._attributePriority.set( key, priority );
  649. return true;
  650. }
  651. // Internal method for setting multiple `LiveSelection` attributes. Supports attribute priorities (through
  652. // `directChange` parameter).
  653. //
  654. // @private
  655. // @param {Map.<String,*>} attrs Iterable object containing attributes to be set.
  656. // @returns {Set.<String>} Changed attribute keys.
  657. _setAttributesTo( attrs ) {
  658. const changed = new Set();
  659. for ( const [ oldKey, oldValue ] of this.getAttributes() ) {
  660. // Do not remove attribute if attribute with same key and value is about to be set.
  661. if ( attrs.get( oldKey ) === oldValue ) {
  662. continue;
  663. }
  664. // All rest attributes will be removed so changed attributes won't change .
  665. this._removeAttribute( oldKey, false );
  666. }
  667. for ( const [ key, value ] of attrs ) {
  668. // Attribute may not be set because of attributes or because same key/value is already added.
  669. const gotAdded = this._setAttribute( key, value, false );
  670. if ( gotAdded ) {
  671. changed.add( key );
  672. }
  673. }
  674. return changed;
  675. }
  676. // Returns an iterable that iterates through all selection attributes stored in current selection's parent.
  677. //
  678. // @protected
  679. // @returns {Iterable.<*>}
  680. * _getStoredAttributes() {
  681. const selectionParent = this.getFirstPosition().parent;
  682. if ( this.isCollapsed && selectionParent.isEmpty ) {
  683. for ( const key of selectionParent.getAttributeKeys() ) {
  684. if ( key.startsWith( storePrefix ) ) {
  685. const realKey = key.substr( storePrefix.length );
  686. yield [ realKey, selectionParent.getAttribute( key ) ];
  687. }
  688. }
  689. }
  690. }
  691. // Checks model text nodes that are closest to the selection's first position and returns attributes of first
  692. // found element. If there are no text nodes in selection's first position parent, it returns selection
  693. // attributes stored in that parent.
  694. //
  695. // @private
  696. // @returns {Iterable.<*>} Collection of attributes.
  697. _getSurroundingAttributes() {
  698. const position = this.getFirstPosition();
  699. const schema = this._model.schema;
  700. let attrs = null;
  701. if ( !this.isCollapsed ) {
  702. // 1. If selection is a range...
  703. const range = this.getFirstRange();
  704. // ...look for a first character node in that range and take attributes from it.
  705. for ( const value of range ) {
  706. // If the item is an object, we don't want to get attributes from its children.
  707. if ( value.item.is( 'element' ) && schema.isObject( value.item ) ) {
  708. break;
  709. }
  710. // This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
  711. // It can be done better by using `break;` instead of checking `attrs === null`.
  712. if ( value.type == 'text' && attrs === null ) {
  713. attrs = value.item.getAttributes();
  714. }
  715. }
  716. } else {
  717. // 2. If the selection is a caret or the range does not contain a character node...
  718. const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
  719. const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
  720. // ...look at the node before caret and take attributes from it if it is a character node.
  721. attrs = getAttrsIfCharacter( nodeBefore );
  722. // 3. If not, look at the node after caret...
  723. if ( !attrs ) {
  724. attrs = getAttrsIfCharacter( nodeAfter );
  725. }
  726. // 4. If not, try to find the first character on the left, that is in the same node.
  727. if ( !attrs ) {
  728. let node = nodeBefore;
  729. while ( node && !attrs ) {
  730. node = node.previousSibling;
  731. attrs = getAttrsIfCharacter( node );
  732. }
  733. }
  734. // 5. If not found, try to find the first character on the right, that is in the same node.
  735. if ( !attrs ) {
  736. let node = nodeAfter;
  737. while ( node && !attrs ) {
  738. node = node.nextSibling;
  739. attrs = getAttrsIfCharacter( node );
  740. }
  741. }
  742. // 6. If not found, selection should retrieve attributes from parent.
  743. if ( !attrs ) {
  744. attrs = this._getStoredAttributes();
  745. }
  746. }
  747. return attrs;
  748. }
  749. // Fixes a selection range after it ends up in graveyard root.
  750. //
  751. // @private
  752. // @param {module:engine/model/liverange~LiveRange} liveRange The range from selection, that ended up in the graveyard root.
  753. // @param {module:engine/model/position~Position} removedRangeStart Start position of a range which was removed.
  754. _fixGraveyardSelection( liveRange, removedRangeStart ) {
  755. // The start of the removed range is the closest position to the `liveRange` - the original selection range.
  756. // This is a good candidate for a fixed selection range.
  757. const positionCandidate = Position.createFromPosition( removedRangeStart );
  758. // Find a range that is a correct selection range and is closest to the start of removed range.
  759. const selectionRange = this._model.schema.getNearestSelectionRange( positionCandidate );
  760. // Remove the old selection range before preparing and adding new selection range. This order is important,
  761. // because new range, in some cases, may intersect with old range (it depends on `getNearestSelectionRange()` result).
  762. const index = this._ranges.indexOf( liveRange );
  763. this._ranges.splice( index, 1 );
  764. liveRange.detach();
  765. // If nearest valid selection range has been found - add it in the place of old range.
  766. if ( selectionRange ) {
  767. // Check the range, convert it to live range, bind events, etc.
  768. const newRange = this._prepareRange( selectionRange );
  769. // Add new range in the place of old range.
  770. this._ranges.splice( index, 0, newRange );
  771. }
  772. // If nearest valid selection range cannot be found - just removing the old range is fine.
  773. // Fire an event informing about selection change.
  774. this.fire( 'change:range', { directChange: false } );
  775. }
  776. }
  777. // Helper function for {@link module:engine/model/liveselection~LiveSelection#_updateAttributes}.
  778. //
  779. // 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`.
  780. //
  781. // @param {module:engine/model/item~Item|null} node
  782. // @returns {Boolean}
  783. function getAttrsIfCharacter( node ) {
  784. if ( node instanceof TextProxy || node instanceof Text ) {
  785. return node.getAttributes();
  786. }
  787. return null;
  788. }
  789. // Removes selection attributes from element which is not empty anymore.
  790. function clearAttributesStoredInElement( operation, model, batch ) {
  791. let changeParent = null;
  792. if ( operation.type == 'insert' ) {
  793. changeParent = operation.position.parent;
  794. } else if ( operation.type == 'move' || operation.type == 'reinsert' || operation.type == 'remove' ) {
  795. changeParent = operation.getMovedRangeStart().parent;
  796. }
  797. if ( !changeParent || changeParent.isEmpty ) {
  798. return;
  799. }
  800. model.enqueueChange( batch, writer => {
  801. const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) );
  802. for ( const key of storedAttributes ) {
  803. writer.removeAttribute( key, changeParent );
  804. }
  805. } );
  806. }