8
0

documentselection.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/documentselection
  7. */
  8. import Position from './position';
  9. import Range from './range';
  10. import LiveRange from './liverange';
  11. import Text from './text';
  12. import TextProxy from './textproxy';
  13. import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
  14. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  15. import log from '@ckeditor/ckeditor5-utils/src/log';
  16. import Selection from './selection';
  17. const storePrefix = 'selection:';
  18. const attrOpTypes = new Set(
  19. [ 'addAttribute', 'removeAttribute', 'changeAttribute', 'addRootAttribute', 'removeRootAttribute', 'changeRootAttribute' ]
  20. );
  21. /**
  22. * `DocumentSelection` is a special selection which is used as the
  23. * {@link module:engine/model/document~Document#selection document's selection}.
  24. * There can be only one instance of `DocumentSelection` per document.
  25. *
  26. * `DocumentSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
  27. * to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
  28. *
  29. * Differences between {@link module:engine/model/selection~Selection} and `DocumentSelection` are:
  30. * * there is always a range in `DocumentSelection` - even if no ranges were added there is a "default range"
  31. * present in the selection,
  32. * * ranges added to this selection updates automatically when the document changes,
  33. * * attributes of `DocumentSelection` are updated automatically according to selection ranges.
  34. *
  35. * Since `DocumentSelection` uses {@link module:engine/model/liverange~LiveRange live ranges}
  36. * and is updated when {@link module:engine/model/document~Document document}
  37. * changes, it cannot be set on {@link module:engine/model/node~Node nodes}
  38. * that are inside {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
  39. * If you need to represent a selection in document fragment,
  40. * use {@link module:engine/model/selection~Selection Selection class} instead.
  41. *
  42. * @extends module:engine/model/selection~Selection
  43. */
  44. export default class DocumentSelection extends Selection {
  45. /**
  46. * Creates an empty live selection for given {@link module:engine/model/document~Document}.
  47. *
  48. * @param {module:engine/model/document~Document} document Document which owns this selection.
  49. * @param {module:engine/model/model~Model} model
  50. */
  51. constructor( document, model ) {
  52. super();
  53. /**
  54. * Document which owns this selection.
  55. *
  56. * @protected
  57. * @member {module:engine/model/model~Model}
  58. */
  59. this._model = model;
  60. /**
  61. * Document which owns this selection.
  62. *
  63. * @protected
  64. * @member {module:engine/model/document~Document}
  65. */
  66. this._document = document;
  67. /**
  68. * Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
  69. * last time. Possible values of priority are: `'low'` and `'normal'`.
  70. *
  71. * Priorities are used by internal `DocumentSelection` mechanisms. All attributes set using `DocumentSelection`
  72. * attributes API are set with `'normal'` priority.
  73. *
  74. * @private
  75. * @member {Map} module:engine/model/documentselection~DocumentSelection#_attributePriority
  76. */
  77. this._attributePriority = new Map();
  78. this.listenTo( this._document, 'change', ( evt, type, changes, batch ) => {
  79. // Whenever attribute operation is performed on document, update selection attributes.
  80. // This is not the most efficient way to update selection attributes, but should be okay for now.
  81. if ( attrOpTypes.has( type ) ) {
  82. this._updateAttributes( false );
  83. }
  84. // Batch may not be passed to the document#change event in some tests.
  85. // See https://github.com/ckeditor/ckeditor5-engine/issues/1001#issuecomment-314202352
  86. // Ignore also transparent batches because they are... transparent.
  87. if ( batch && batch.type !== 'transparent' ) {
  88. // Whenever element which had selection's attributes stored in it stops being empty,
  89. // the attributes need to be removed.
  90. clearAttributesStoredInElement( changes, this._model );
  91. }
  92. } );
  93. }
  94. /**
  95. * @inheritDoc
  96. */
  97. get isCollapsed() {
  98. const length = this._ranges.length;
  99. return length === 0 ? this._document._getDefaultRange().isCollapsed : super.isCollapsed;
  100. }
  101. /**
  102. * @inheritDoc
  103. */
  104. get anchor() {
  105. return super.anchor || this._document._getDefaultRange().start;
  106. }
  107. /**
  108. * @inheritDoc
  109. */
  110. get focus() {
  111. return super.focus || this._document._getDefaultRange().end;
  112. }
  113. /**
  114. * @inheritDoc
  115. */
  116. get rangeCount() {
  117. return this._ranges.length ? this._ranges.length : 1;
  118. }
  119. /**
  120. * Describes whether `DocumentSelection` has own range(s) set, or if it is defaulted to
  121. * {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
  122. *
  123. * @readonly
  124. * @type {Boolean}
  125. */
  126. get hasOwnRange() {
  127. return this._ranges.length > 0;
  128. }
  129. /**
  130. * Unbinds all events previously bound by document selection.
  131. */
  132. destroy() {
  133. for ( let i = 0; i < this._ranges.length; i++ ) {
  134. this._ranges[ i ].detach();
  135. }
  136. this.stopListening();
  137. }
  138. /**
  139. * @inheritDoc
  140. */
  141. * getRanges() {
  142. if ( this._ranges.length ) {
  143. yield* super.getRanges();
  144. } else {
  145. yield this._document._getDefaultRange();
  146. }
  147. }
  148. /**
  149. * @inheritDoc
  150. */
  151. getFirstRange() {
  152. return super.getFirstRange() || this._document._getDefaultRange();
  153. }
  154. /**
  155. * @inheritDoc
  156. */
  157. getLastRange() {
  158. return super.getLastRange() || this._document._getDefaultRange();
  159. }
  160. /**
  161. * @inheritDoc
  162. */
  163. addRange( range, isBackward = false ) {
  164. super.addRange( range, isBackward );
  165. this.refreshAttributes();
  166. }
  167. /**
  168. * @inheritDoc
  169. */
  170. removeAllRanges() {
  171. super.removeAllRanges();
  172. this.refreshAttributes();
  173. }
  174. /**
  175. * @inheritDoc
  176. */
  177. setRanges( newRanges, isLastBackward = false ) {
  178. super.setRanges( newRanges, isLastBackward );
  179. this.refreshAttributes();
  180. }
  181. /**
  182. * @inheritDoc
  183. */
  184. setAttribute( key, value ) {
  185. // Store attribute in parent element if the selection is collapsed in an empty node.
  186. if ( this.isCollapsed && this.anchor.parent.isEmpty ) {
  187. this._storeAttribute( key, value );
  188. }
  189. if ( this._setAttribute( key, value ) ) {
  190. // Fire event with exact data.
  191. const attributeKeys = [ key ];
  192. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  193. }
  194. }
  195. /**
  196. * @inheritDoc
  197. */
  198. removeAttribute( key ) {
  199. // Remove stored attribute from parent element if the selection is collapsed in an empty node.
  200. if ( this.isCollapsed && this.anchor.parent.isEmpty ) {
  201. this._removeStoredAttribute( key );
  202. }
  203. if ( this._removeAttribute( key ) ) {
  204. // Fire event with exact data.
  205. const attributeKeys = [ key ];
  206. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  207. }
  208. }
  209. /**
  210. * @inheritDoc
  211. */
  212. setAttributesTo( attrs ) {
  213. attrs = toMap( attrs );
  214. if ( this.isCollapsed && this.anchor.parent.isEmpty ) {
  215. this._setStoredAttributesTo( attrs );
  216. }
  217. const changed = this._setAttributesTo( attrs );
  218. if ( changed.size > 0 ) {
  219. // Fire event with exact data (fire only if anything changed).
  220. const attributeKeys = Array.from( changed );
  221. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  222. }
  223. }
  224. /**
  225. * @inheritDoc
  226. */
  227. clearAttributes() {
  228. this.setAttributesTo( [] );
  229. }
  230. /**
  231. * Removes all attributes from the selection and sets attributes according to the surrounding nodes.
  232. */
  233. refreshAttributes() {
  234. this._updateAttributes( true );
  235. }
  236. /**
  237. * This method is not available in `DocumentSelection`. There can be only one
  238. * `DocumentSelection` per document instance, so creating new `DocumentSelection`s this way
  239. * would be unsafe.
  240. */
  241. static createFromSelection() {
  242. /**
  243. * Cannot create a new `DocumentSelection` instance.
  244. *
  245. * `DocumentSelection#createFromSelection()` is not available. There can be only one
  246. * `DocumentSelection` per document instance, so creating new `DocumentSelection`s this way
  247. * would be unsafe.
  248. *
  249. * @error documentselection-cannot-create
  250. */
  251. throw new CKEditorError( 'documentselection-cannot-create: Cannot create a new DocumentSelection instance.' );
  252. }
  253. /**
  254. * @inheritDoc
  255. */
  256. _popRange() {
  257. this._ranges.pop().detach();
  258. }
  259. /**
  260. * @inheritDoc
  261. */
  262. _pushRange( range ) {
  263. const liveRange = this._prepareRange( range );
  264. // `undefined` is returned when given `range` is in graveyard root.
  265. if ( liveRange ) {
  266. this._ranges.push( liveRange );
  267. }
  268. }
  269. /**
  270. * Prepares given range to be added to selection. Checks if it is correct,
  271. * converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
  272. * and sets listeners listening to the range's change event.
  273. *
  274. * @private
  275. * @param {module:engine/model/range~Range} range
  276. */
  277. _prepareRange( range ) {
  278. if ( !( range instanceof Range ) ) {
  279. /**
  280. * Trying to add an object that is not an instance of Range.
  281. *
  282. * @error model-selection-added-not-range
  283. */
  284. throw new CKEditorError( 'model-selection-added-not-range: Trying to add an object that is not an instance of Range.' );
  285. }
  286. if ( range.root == this._document.graveyard ) {
  287. /**
  288. * Trying to add a Range that is in the graveyard root. Range rejected.
  289. *
  290. * @warning model-selection-range-in-graveyard
  291. */
  292. log.warn( 'model-selection-range-in-graveyard: Trying to add a Range that is in the graveyard root. Range rejected.' );
  293. return;
  294. }
  295. this._checkRange( range );
  296. const liveRange = LiveRange.createFromRange( range );
  297. this.listenTo( liveRange, 'change:range', ( evt, oldRange, data ) => {
  298. // If `LiveRange` is in whole moved to the graveyard, fix that range.
  299. if ( liveRange.root == this._document.graveyard ) {
  300. this._fixGraveyardSelection( liveRange, data.sourcePosition );
  301. }
  302. // Whenever a live range from selection changes, fire an event informing about that change.
  303. this.fire( 'change:range', { directChange: false } );
  304. } );
  305. return liveRange;
  306. }
  307. /**
  308. * Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
  309. *
  310. * @protected
  311. * @param {Boolean} clearAll
  312. * @fires change:attribute
  313. */
  314. _updateAttributes( clearAll ) {
  315. const newAttributes = toMap( this._getSurroundingAttributes() );
  316. const oldAttributes = toMap( this.getAttributes() );
  317. if ( clearAll ) {
  318. // If `clearAll` remove all attributes and reset priorities.
  319. this._attributePriority = new Map();
  320. this._attrs = new Map();
  321. } else {
  322. // If not, remove only attributes added with `low` priority.
  323. for ( const [ key, priority ] of this._attributePriority ) {
  324. if ( priority == 'low' ) {
  325. this._attrs.delete( key );
  326. this._attributePriority.delete( key );
  327. }
  328. }
  329. }
  330. this._setAttributesTo( newAttributes, false );
  331. // Let's evaluate which attributes really changed.
  332. const changed = [];
  333. // First, loop through all attributes that are set on selection right now.
  334. // Check which of them are different than old attributes.
  335. for ( const [ newKey, newValue ] of this.getAttributes() ) {
  336. if ( !oldAttributes.has( newKey ) || oldAttributes.get( newKey ) !== newValue ) {
  337. changed.push( newKey );
  338. }
  339. }
  340. // Then, check which of old attributes got removed.
  341. for ( const [ oldKey ] of oldAttributes ) {
  342. if ( !this.hasAttribute( oldKey ) ) {
  343. changed.push( oldKey );
  344. }
  345. }
  346. // Fire event with exact data (fire only if anything changed).
  347. if ( changed.length > 0 ) {
  348. this.fire( 'change:attribute', { attributeKeys: changed, directChange: false } );
  349. }
  350. }
  351. /**
  352. * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
  353. *
  354. * @protected
  355. * @param {String} key Attribute key to convert.
  356. * @returns {String} Converted attribute key, applicable for selection store.
  357. */
  358. static _getStoreAttributeKey( key ) {
  359. return storePrefix + key;
  360. }
  361. /**
  362. * Checks whether the given attribute key is an attribute stored on an element.
  363. *
  364. * @protected
  365. * @param {String} key
  366. * @returns {Boolean}
  367. */
  368. static _isStoreAttributeKey( key ) {
  369. return key.startsWith( storePrefix );
  370. }
  371. /**
  372. * Internal method for setting `DocumentSelection` attribute. Supports attribute priorities (through `directChange`
  373. * parameter).
  374. *
  375. * @private
  376. * @param {String} key Attribute key.
  377. * @param {*} value Attribute value.
  378. * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
  379. * is caused by `Batch` API.
  380. * @returns {Boolean} Whether value has changed.
  381. */
  382. _setAttribute( key, value, directChange = true ) {
  383. const priority = directChange ? 'normal' : 'low';
  384. if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
  385. // Priority too low.
  386. return false;
  387. }
  388. const oldValue = super.getAttribute( key );
  389. // Don't do anything if value has not changed.
  390. if ( oldValue === value ) {
  391. return false;
  392. }
  393. this._attrs.set( key, value );
  394. // Update priorities map.
  395. this._attributePriority.set( key, priority );
  396. return true;
  397. }
  398. /**
  399. * Internal method for removing `DocumentSelection` attribute. Supports attribute priorities (through `directChange`
  400. * parameter).
  401. *
  402. * @private
  403. * @param {String} key Attribute key.
  404. * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
  405. * is caused by `Batch` API.
  406. * @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
  407. * existing attribute had higher priority.
  408. */
  409. _removeAttribute( key, directChange = true ) {
  410. const priority = directChange ? 'normal' : 'low';
  411. if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
  412. // Priority too low.
  413. return false;
  414. }
  415. // Don't do anything if value has not changed.
  416. if ( !super.hasAttribute( key ) ) {
  417. return false;
  418. }
  419. this._attrs.delete( key );
  420. // Update priorities map.
  421. this._attributePriority.set( key, priority );
  422. return true;
  423. }
  424. /**
  425. * Internal method for setting multiple `DocumentSelection` attributes. Supports attribute priorities (through
  426. * `directChange` parameter).
  427. *
  428. * @private
  429. * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
  430. * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
  431. * is caused by `Batch` API.
  432. * @returns {Set.<String>} Changed attribute keys.
  433. */
  434. _setAttributesTo( attrs, directChange = true ) {
  435. const changed = new Set();
  436. for ( const [ oldKey, oldValue ] of this.getAttributes() ) {
  437. // Do not remove attribute if attribute with same key and value is about to be set.
  438. if ( attrs.get( oldKey ) === oldValue ) {
  439. continue;
  440. }
  441. // Attribute still might not get removed because of priorities.
  442. if ( this._removeAttribute( oldKey, directChange ) ) {
  443. changed.add( oldKey );
  444. }
  445. }
  446. for ( const [ key, value ] of attrs ) {
  447. // Attribute may not be set because of attributes or because same key/value is already added.
  448. const gotAdded = this._setAttribute( key, value, directChange );
  449. if ( gotAdded ) {
  450. changed.add( key );
  451. }
  452. }
  453. return changed;
  454. }
  455. /**
  456. * Returns an iterator that iterates through all selection attributes stored in current selection's parent.
  457. *
  458. * @private
  459. * @returns {Iterable.<*>}
  460. */
  461. * _getStoredAttributes() {
  462. const selectionParent = this.getFirstPosition().parent;
  463. if ( this.isCollapsed && selectionParent.isEmpty ) {
  464. for ( const key of selectionParent.getAttributeKeys() ) {
  465. if ( key.startsWith( storePrefix ) ) {
  466. const realKey = key.substr( storePrefix.length );
  467. yield [ realKey, selectionParent.getAttribute( key ) ];
  468. }
  469. }
  470. }
  471. }
  472. /**
  473. * Removes attribute with given key from attributes stored in current selection's parent node.
  474. *
  475. * @private
  476. * @param {String} key Key of attribute to remove.
  477. */
  478. _removeStoredAttribute( key ) {
  479. const storeKey = DocumentSelection._getStoreAttributeKey( key );
  480. this._model.change( writer => {
  481. writer.removeAttribute( storeKey, this.anchor.parent );
  482. } );
  483. }
  484. /**
  485. * Stores given attribute key and value in current selection's parent node.
  486. *
  487. * @private
  488. * @param {String} key Key of attribute to set.
  489. * @param {*} value Attribute value.
  490. */
  491. _storeAttribute( key, value ) {
  492. const storeKey = DocumentSelection._getStoreAttributeKey( key );
  493. this._model.change( writer => {
  494. writer.setAttribute( storeKey, value, this.anchor.parent );
  495. } );
  496. }
  497. /**
  498. * Sets selection attributes stored in current selection's parent node to given set of attributes.
  499. *
  500. * @private
  501. * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
  502. */
  503. _setStoredAttributesTo( attrs ) {
  504. const selectionParent = this.anchor.parent;
  505. this._model.change( writer => {
  506. for ( const [ oldKey ] of this._getStoredAttributes() ) {
  507. const storeKey = DocumentSelection._getStoreAttributeKey( oldKey );
  508. writer.removeAttribute( storeKey, selectionParent );
  509. }
  510. for ( const [ key, value ] of attrs ) {
  511. const storeKey = DocumentSelection._getStoreAttributeKey( key );
  512. writer.setAttribute( storeKey, value, selectionParent );
  513. }
  514. } );
  515. }
  516. /**
  517. * Checks model text nodes that are closest to the selection's first position and returns attributes of first
  518. * found element. If there are no text nodes in selection's first position parent, it returns selection
  519. * attributes stored in that parent.
  520. *
  521. * @private
  522. * @returns {Iterable.<*>} Collection of attributes.
  523. */
  524. _getSurroundingAttributes() {
  525. const position = this.getFirstPosition();
  526. const schema = this._document.schema;
  527. let attrs = null;
  528. if ( !this.isCollapsed ) {
  529. // 1. If selection is a range...
  530. const range = this.getFirstRange();
  531. // ...look for a first character node in that range and take attributes from it.
  532. for ( const value of range ) {
  533. // If the item is an object, we don't want to get attributes from its children.
  534. if ( value.item.is( 'element' ) && schema.objects.has( value.item.name ) ) {
  535. break;
  536. }
  537. // This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
  538. // It can be done better by using `break;` instead of checking `attrs === null`.
  539. if ( value.type == 'text' && attrs === null ) {
  540. attrs = value.item.getAttributes();
  541. }
  542. }
  543. } else {
  544. // 2. If the selection is a caret or the range does not contain a character node...
  545. const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
  546. const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
  547. // ...look at the node before caret and take attributes from it if it is a character node.
  548. attrs = getAttrsIfCharacter( nodeBefore );
  549. // 3. If not, look at the node after caret...
  550. if ( !attrs ) {
  551. attrs = getAttrsIfCharacter( nodeAfter );
  552. }
  553. // 4. If not, try to find the first character on the left, that is in the same node.
  554. if ( !attrs ) {
  555. let node = nodeBefore;
  556. while ( node && !attrs ) {
  557. node = node.previousSibling;
  558. attrs = getAttrsIfCharacter( node );
  559. }
  560. }
  561. // 5. If not found, try to find the first character on the right, that is in the same node.
  562. if ( !attrs ) {
  563. let node = nodeAfter;
  564. while ( node && !attrs ) {
  565. node = node.nextSibling;
  566. attrs = getAttrsIfCharacter( node );
  567. }
  568. }
  569. // 6. If not found, selection should retrieve attributes from parent.
  570. if ( !attrs ) {
  571. attrs = this._getStoredAttributes();
  572. }
  573. }
  574. return attrs;
  575. }
  576. /**
  577. * Fixes a selection range after it ends up in graveyard root.
  578. *
  579. * @private
  580. * @param {module:engine/model/liverange~LiveRange} liveRange The range from selection, that ended up in the graveyard root.
  581. * @param {module:engine/model/position~Position} removedRangeStart Start position of a range which was removed.
  582. */
  583. _fixGraveyardSelection( liveRange, removedRangeStart ) {
  584. // The start of the removed range is the closest position to the `liveRange` - the original selection range.
  585. // This is a good candidate for a fixed selection range.
  586. const positionCandidate = Position.createFromPosition( removedRangeStart );
  587. // Find a range that is a correct selection range and is closest to the start of removed range.
  588. const selectionRange = this._document.getNearestSelectionRange( positionCandidate );
  589. // Remove the old selection range before preparing and adding new selection range. This order is important,
  590. // because new range, in some cases, may intersect with old range (it depends on `getNearestSelectionRange()` result).
  591. const index = this._ranges.indexOf( liveRange );
  592. this._ranges.splice( index, 1 );
  593. liveRange.detach();
  594. // If nearest valid selection range has been found - add it in the place of old range.
  595. if ( selectionRange ) {
  596. // Check the range, convert it to live range, bind events, etc.
  597. const newRange = this._prepareRange( selectionRange );
  598. // Add new range in the place of old range.
  599. this._ranges.splice( index, 0, newRange );
  600. }
  601. // If nearest valid selection range cannot be found - just removing the old range is fine.
  602. // Fire an event informing about selection change.
  603. this.fire( 'change:range', { directChange: false } );
  604. }
  605. }
  606. /**
  607. * @event change:attribute
  608. */
  609. // Helper function for {@link module:engine/model/documentselection~DocumentSelection#_updateAttributes}.
  610. //
  611. // 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`.
  612. //
  613. // @param {module:engine/model/item~Item|null} node
  614. // @returns {Boolean}
  615. function getAttrsIfCharacter( node ) {
  616. if ( node instanceof TextProxy || node instanceof Text ) {
  617. return node.getAttributes();
  618. }
  619. return null;
  620. }
  621. // Removes selection attributes from element which is not empty anymore.
  622. function clearAttributesStoredInElement( changes, model ) {
  623. const changeParent = changes.range && changes.range.start.parent;
  624. // `changes.range` is not set in case of rename, root and marker operations.
  625. // None of them may lead to the element becoming non-empty.
  626. if ( !changeParent || changeParent.isEmpty ) {
  627. return;
  628. }
  629. model.change( writer => {
  630. const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) );
  631. for ( const key of storedAttributes ) {
  632. writer.removeAttribute( key, changeParent );
  633. }
  634. } );
  635. }