liveselection.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Range from './range.js';
  6. import LiveRange from './liverange.js';
  7. import Text from './text.js';
  8. import TextProxy from './textproxy.js';
  9. import toMap from '../../utils/tomap.js';
  10. import CKEditorError from '../../utils/ckeditorerror.js';
  11. import Selection from './selection.js';
  12. const storePrefix = 'selection:';
  13. const attrOpTypes = new Set(
  14. [ 'addAttribute', 'removeAttribute', 'changeAttribute', 'addRootAttribute', 'removeRootAttribute', 'changeRootAttribute' ]
  15. );
  16. /**
  17. * `LiveSelection` is a type of {@link engine.model.Selection selection} that listens to changes on a
  18. * {@link engine.model.Document document} and has it ranges updated accordingly. Internal implementation of this
  19. * mechanism bases on {@link engine.model.LiveRange live ranges}.
  20. *
  21. * Differences between {@link engine.model.Selection} and `LiveSelection` are two:
  22. * * there is always a range in `LiveSelection` - even if no ranges were added there is a
  23. * {@link engine.model.LiveSelection#_getDefaultRange "default range"} present in the selection,
  24. * * ranges added to this selection updates automatically when the document changes.
  25. *
  26. * Since `LiveSelection` uses {@link engine.model.LiveRange live ranges} and is updated when {@link engine.model.Document document}
  27. * changes, it cannot be set on {@link engine.model.Node nodes} that are inside {@link engine.model.DocumentFragment document fragment}.
  28. * If you need to represent a selection in document fragment, use {@link engine.model.Selection "normal" selection} instead.
  29. *
  30. * @memberOf engine.model
  31. */
  32. export default class LiveSelection extends Selection {
  33. /**
  34. * Creates an empty live selection for given {@link engine.model.Document}.
  35. *
  36. * @param {engine.model.Document} document Document which owns this selection.
  37. */
  38. constructor( document ) {
  39. super();
  40. /**
  41. * Document which owns this selection.
  42. *
  43. * @protected
  44. * @member {engine.model.Document} engine.model.LiveSelection#_document
  45. */
  46. this._document = document;
  47. /**
  48. * Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
  49. * last time. Possible values of priority are: `'low'` and `'normal'`.
  50. *
  51. * Priorities are used by internal `LiveSelection` mechanisms. All attributes set using `LiveSelection`
  52. * attributes API are set with `'normal'` priority.
  53. *
  54. * @private
  55. * @member {Map} engine.model.LiveSelection#_attributePriority
  56. */
  57. this._attributePriority = new Map();
  58. // Whenever selection range changes, if the change comes directly from selection API (direct user change).
  59. this.on( 'change:range', ( evt, data ) => {
  60. if ( data.directChange ) {
  61. // Reset attributes on selection (clear attributes and priorities).
  62. //this._resetAttributes();
  63. // And get attributes from surrounding nodes.
  64. this._updateAttributes( true );
  65. }
  66. }, { priority: 'high' } );
  67. // Whenever attribute operation is performed on document, update attributes. This is not the most efficient
  68. // way to update selection attributes, but should be okay for now. `_updateAttributes` will be fired too often,
  69. // but it won't change attributes or fire `change:attribute` event if not needed.
  70. this.listenTo( this._document, 'change', ( evt, type ) => {
  71. if ( attrOpTypes.has( type ) ) {
  72. this._updateAttributes( false );
  73. }
  74. } );
  75. }
  76. /**
  77. * @inheritDoc
  78. */
  79. get isCollapsed() {
  80. const length = this._ranges.length;
  81. return length === 0 ? true : super.isCollapsed;
  82. }
  83. /**
  84. * @inheritDoc
  85. */
  86. get anchor() {
  87. return super.anchor || this._document._getDefaultRange().start;
  88. }
  89. /**
  90. * @inheritDoc
  91. */
  92. get focus() {
  93. return super.focus || this._document._getDefaultRange().start;
  94. }
  95. /**
  96. * @inheritDoc
  97. */
  98. get rangeCount() {
  99. return this._ranges.length ? this._ranges.length : 1;
  100. }
  101. /**
  102. * Unbinds all events previously bound by document selection.
  103. */
  104. destroy() {
  105. for ( let i = 0; i < this._ranges.length; i++ ) {
  106. this._ranges[ i ].detach();
  107. }
  108. this.stopListening();
  109. }
  110. /**
  111. * @inheritDoc
  112. */
  113. *getRanges() {
  114. if ( this._ranges.length ) {
  115. yield *super.getRanges();
  116. } else {
  117. yield this._document._getDefaultRange();
  118. }
  119. }
  120. /**
  121. * @inheritDoc
  122. */
  123. getFirstRange() {
  124. return super.getFirstRange() || this._document._getDefaultRange();
  125. }
  126. /**
  127. * @inheritDoc
  128. */
  129. getLastRange() {
  130. return super.getLastRange() || this._document._getDefaultRange();
  131. }
  132. /**
  133. * @inheritDoc
  134. */
  135. setAttribute( key, value ) {
  136. // Store attribute in parent element if the selection is collapsed in an empty node.
  137. if ( this.isCollapsed && this.anchor.parent.childCount === 0 ) {
  138. this._storeAttribute( key, value );
  139. }
  140. if ( this._setAttribute( key, value ) ) {
  141. // Fire event with exact data.
  142. const attributeKeys = [ key ];
  143. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  144. }
  145. }
  146. /**
  147. * @inheritDoc
  148. */
  149. removeAttribute( key ) {
  150. // Remove stored attribute from parent element if the selection is collapsed in an empty node.
  151. if ( this.isCollapsed && this.anchor.parent.childCount === 0 ) {
  152. this._removeStoredAttribute( key );
  153. }
  154. if ( this._removeAttribute( key ) ) {
  155. // Fire event with exact data.
  156. const attributeKeys = [ key ];
  157. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  158. }
  159. }
  160. /**
  161. * @inheritDoc
  162. */
  163. setAttributesTo( attrs ) {
  164. attrs = toMap( attrs );
  165. if ( this.isCollapsed && this.anchor.parent.childCount === 0 ) {
  166. this._setStoredAttributesTo( attrs );
  167. }
  168. const changed = this._setAttributesTo( attrs );
  169. if ( changed.size > 0 ) {
  170. // Fire event with exact data (fire only if anything changed).
  171. const attributeKeys = Array.from( changed );
  172. this.fire( 'change:attribute', { attributeKeys, directChange: true } );
  173. }
  174. }
  175. /**
  176. * @inheritDoc
  177. */
  178. clearAttributes() {
  179. this.setAttributesTo( [] );
  180. }
  181. /**
  182. * Creates and returns an instance of `LiveSelection` that is a clone of given selection, meaning that it has same
  183. * ranges and same direction as this selection.
  184. *
  185. * @params {engine.model.Selection} otherSelection Selection to be cloned.
  186. * @returns {engine.model.LiveSelection} `Selection` instance that is a clone of given selection.
  187. */
  188. static createFromSelection( otherSelection ) {
  189. const selection = new this( otherSelection._document );
  190. selection.setTo( otherSelection );
  191. return selection;
  192. }
  193. /**
  194. * @inheritDoc
  195. */
  196. _popRange() {
  197. this._ranges.pop().detach();
  198. }
  199. /**
  200. * @inheritDoc
  201. */
  202. _pushRange( range ) {
  203. if ( !( range instanceof Range ) ) {
  204. throw new CKEditorError( 'model-selection-added-not-range: Trying to add an object that is not an instance of Range.' );
  205. }
  206. this._checkRange( range );
  207. const liveRange = LiveRange.createFromRange( range );
  208. this.listenTo( liveRange, 'change', () => {
  209. this.fire( 'change:range', { directChange: false } );
  210. } );
  211. this._ranges.push( liveRange );
  212. }
  213. /**
  214. * Updates this selection attributes according to it's ranges and the {@link engine.model.Document model document}.
  215. *
  216. * @protected
  217. * @param {Boolean} clearAll
  218. * @fires engine.model.LiveSelection#change:attribute
  219. */
  220. _updateAttributes( clearAll ) {
  221. const newAttributes = toMap( this._getSurroundingAttributes() );
  222. const oldAttributes = toMap( this.getAttributes() );
  223. if ( clearAll ) {
  224. // If `clearAll` remove all attributes and reset priorities.
  225. this._attributePriority = new Map();
  226. this._attrs = new Map();
  227. } else {
  228. // If not, remove only attributes added with `low` priority.
  229. for ( let [ key, priority ] of this._attributePriority ) {
  230. if ( priority == 'low' ) {
  231. this._attrs.delete( key );
  232. this._attributePriority.delete( key );
  233. }
  234. }
  235. }
  236. this._setAttributesTo( newAttributes, false );
  237. // Let's evaluate which attributes really changed.
  238. const changed = [];
  239. // First, loop through all attributes that are set on selection right now.
  240. // Check which of them are different than old attributes.
  241. for ( let [ newKey, newValue ] of this.getAttributes() ) {
  242. if ( !oldAttributes.has( newKey ) || oldAttributes.get( newKey ) !== newValue ) {
  243. changed.push( newKey );
  244. }
  245. }
  246. // Then, check which of old attributes got removed.
  247. for ( let [ oldKey ] of oldAttributes ) {
  248. if ( !this.hasAttribute( oldKey ) ) {
  249. changed.push( oldKey );
  250. }
  251. }
  252. // Fire event with exact data (fire only if anything changed).
  253. if ( changed.length > 0 ) {
  254. this.fire( 'change:attribute', { attributeKeys: changed, directChange: false } );
  255. }
  256. }
  257. /**
  258. * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
  259. *
  260. * @protected
  261. * @param {String} key Attribute key to convert.
  262. * @returns {String} Converted attribute key, applicable for selection store.
  263. */
  264. static _getStoreAttributeKey( key ) {
  265. return storePrefix + key;
  266. }
  267. /**
  268. * Internal method for setting `LiveSelection` attribute. Supports attribute priorities (through `directChange`
  269. * parameter).
  270. *
  271. * @private
  272. * @param {String} key Attribute key.
  273. * @param {*} value Attribute value.
  274. * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
  275. * is caused by `Batch` API.
  276. * @returns {Boolean} Whether value has changed.
  277. */
  278. _setAttribute( key, value, directChange = true ) {
  279. const priority = directChange ? 'normal' : 'low';
  280. if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
  281. // Priority too low.
  282. return false;
  283. }
  284. const oldValue = super.getAttribute( key );
  285. // Don't do anything if value has not changed.
  286. if ( oldValue === value ) {
  287. return false;
  288. }
  289. this._attrs.set( key, value );
  290. // Update priorities map.
  291. this._attributePriority.set( key, priority );
  292. return true;
  293. }
  294. /**
  295. * Internal method for removing `LiveSelection` attribute. Supports attribute priorities (through `directChange`
  296. * parameter).
  297. *
  298. * @private
  299. * @param {String} key Attribute key.
  300. * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
  301. * is caused by `Batch` API.
  302. * @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
  303. * existing attribute had higher priority.
  304. */
  305. _removeAttribute( key, directChange = true ) {
  306. const priority = directChange ? 'normal' : 'low';
  307. if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
  308. // Priority too low.
  309. return false;
  310. }
  311. // Don't do anything if value has not changed.
  312. if ( !super.hasAttribute( key ) ) {
  313. return false;
  314. }
  315. this._attrs.delete( key );
  316. // Update priorities map.
  317. this._attributePriority.set( key, priority );
  318. return true;
  319. }
  320. /**
  321. * Internal method for setting multiple `LiveSelection` attributes. Supports attribute priorities (through
  322. * `directChange` parameter).
  323. *
  324. * @private
  325. * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
  326. * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
  327. * is caused by `Batch` API.
  328. * @returns {Set.<String>} Changed attribute keys.
  329. */
  330. _setAttributesTo( attrs, directChange = true ) {
  331. const changed = new Set();
  332. for ( let [ oldKey, oldValue ] of this.getAttributes() ) {
  333. // Do not remove attribute if attribute with same key and value is about to be set.
  334. if ( attrs.get( oldKey ) === oldValue ) {
  335. continue;
  336. }
  337. // Attribute still might not get removed because of priorities.
  338. if ( this._removeAttribute( oldKey, directChange ) ) {
  339. changed.add( oldKey );
  340. }
  341. }
  342. for ( let [ key, value ] of attrs ) {
  343. // Attribute may not be set because of attributes or because same key/value is already added.
  344. const gotAdded = this._setAttribute( key, value, directChange );
  345. if ( gotAdded ) {
  346. changed.add( key );
  347. }
  348. }
  349. return changed;
  350. }
  351. /**
  352. * Returns an iterator that iterates through all selection attributes stored in current selection's parent.
  353. *
  354. * @private
  355. * @returns {Iterable.<*>}
  356. */
  357. *_getStoredAttributes() {
  358. const selectionParent = this.getFirstPosition().parent;
  359. if ( this.isCollapsed && selectionParent.childCount === 0 ) {
  360. for ( let key of selectionParent.getAttributeKeys() ) {
  361. if ( key.indexOf( storePrefix ) === 0 ) {
  362. const realKey = key.substr( storePrefix.length );
  363. yield [ realKey, selectionParent.getAttribute( key ) ];
  364. }
  365. }
  366. }
  367. }
  368. /**
  369. * Removes attribute with given key from attributes stored in current selection's parent node.
  370. *
  371. * @private
  372. * @param {String} key Key of attribute to remove.
  373. */
  374. _removeStoredAttribute( key ) {
  375. const storeKey = LiveSelection._getStoreAttributeKey( key );
  376. this._document.batch().removeAttribute( this.anchor.parent, storeKey );
  377. }
  378. /**
  379. * Stores given attribute key and value in current selection's parent node.
  380. *
  381. * @private
  382. * @param {String} key Key of attribute to set.
  383. * @param {*} value Attribute value.
  384. */
  385. _storeAttribute( key, value ) {
  386. const storeKey = LiveSelection._getStoreAttributeKey( key );
  387. this._document.batch().setAttribute( this.anchor.parent, storeKey, value );
  388. }
  389. /**
  390. * Sets selection attributes stored in current selection's parent node to given set of attributes.
  391. *
  392. * @private
  393. * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
  394. */
  395. _setStoredAttributesTo( attrs ) {
  396. const selectionParent = this.anchor.parent;
  397. const batch = this._document.batch();
  398. for ( let [ oldKey ] of this._getStoredAttributes() ) {
  399. const storeKey = LiveSelection._getStoreAttributeKey( oldKey );
  400. batch.removeAttribute( selectionParent, storeKey );
  401. }
  402. for ( let [ key, value ] of attrs ) {
  403. const storeKey = LiveSelection._getStoreAttributeKey( key );
  404. batch.setAttribute( selectionParent, storeKey, value );
  405. }
  406. }
  407. /**
  408. * Checks model text nodes that are closest to the selection's first position and returns attributes of first
  409. * found element. If there are no text nodes in selection's first position parent, it returns selection
  410. * attributes stored in that parent.
  411. *
  412. * @private
  413. * @returns {Iterable.<*>} Collection of attributes.
  414. */
  415. _getSurroundingAttributes() {
  416. const position = this.getFirstPosition();
  417. let attrs = null;
  418. if ( !this.isCollapsed ) {
  419. // 1. If selection is a range...
  420. const range = this.getFirstRange();
  421. // ...look for a first character node in that range and take attributes from it.
  422. for ( let item of range ) {
  423. // This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
  424. // It can be done better by using `break;` instead of checking `attrs === null`.
  425. if ( item.type == 'text' && attrs === null ) {
  426. attrs = item.item.getAttributes();
  427. }
  428. }
  429. } else {
  430. // 2. If the selection is a caret or the range does not contain a character node...
  431. const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
  432. const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
  433. // ...look at the node before caret and take attributes from it if it is a character node.
  434. attrs = _getAttrsIfCharacter( nodeBefore );
  435. // 3. If not, look at the node after caret...
  436. if ( !attrs ) {
  437. attrs = _getAttrsIfCharacter( nodeAfter );
  438. }
  439. // 4. If not, try to find the first character on the left, that is in the same node.
  440. if ( !attrs ) {
  441. let node = nodeBefore;
  442. while ( node && !attrs ) {
  443. node = node.previousSibling;
  444. attrs = _getAttrsIfCharacter( node );
  445. }
  446. }
  447. // 5. If not found, try to find the first character on the right, that is in the same node.
  448. if ( !attrs ) {
  449. let node = nodeAfter;
  450. while ( node && !attrs ) {
  451. node = node.nextSibling;
  452. attrs = _getAttrsIfCharacter( node );
  453. }
  454. }
  455. // 6. If not found, selection should retrieve attributes from parent.
  456. if ( !attrs ) {
  457. attrs = this._getStoredAttributes();
  458. }
  459. }
  460. return attrs;
  461. }
  462. }
  463. // Helper function for {@link engine.model.LiveSelection#_updateAttributes}. It takes model item, checks whether
  464. // it is a text node (or text proxy) and if so, returns it's attributes. If not, returns `null`.
  465. //
  466. // @param {engine.model.Item} node
  467. // @returns {Boolean}
  468. function _getAttrsIfCharacter( node ) {
  469. if ( node instanceof TextProxy || node instanceof Text ) {
  470. return node.getAttributes();
  471. }
  472. return null;
  473. }