8
0

documentselection.js 36 KB

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