liveselection.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import LiveRange from './liverange.js';
  7. import Range from './range.js';
  8. import Position from './position.js';
  9. import CharacterProxy from './characterproxy.js';
  10. import toMap from '../../utils/tomap.js';
  11. import Selection from './selection.js';
  12. const storePrefix = 'selection:';
  13. /**
  14. * `LiveSelection` is a special type of {@link engine.model.Selection selection} that listens to changes on a
  15. * {@link engine.model.Document document} and has it ranges updated accordingly. Internal implementation of this
  16. * mechanism bases on {@link engine.model.LiveRange live ranges}.
  17. *
  18. * Differences between {@link engine.model.Selection} and `LiveSelection` are three:
  19. * * there is always a range in `LiveSelection`, even if no ranges were added - in this case, there is a
  20. * "default range" in selection which is a collapsed range set at the beginning of the {@link engine.model.Document document},
  21. * * ranges added to this selection updates automatically when the document changes,
  22. * * live selection may have attributes.
  23. *
  24. * @memberOf engine.model
  25. */
  26. export default class LiveSelection extends Selection {
  27. /**
  28. * Creates an empty document selection for given {@link engine.model.Document}.
  29. *
  30. * @param {engine.model.Document} document Document which owns this selection.
  31. */
  32. constructor( document ) {
  33. super();
  34. /**
  35. * Document which owns this selection.
  36. *
  37. * @private
  38. * @member {engine.model.Document} engine.model.Selection#_document
  39. */
  40. this._document = document;
  41. /**
  42. * List of attributes set on current selection.
  43. *
  44. * @protected
  45. * @member {Map} engine.model.LiveSelection#_attrs
  46. */
  47. this._attrs = new Map();
  48. }
  49. /**
  50. * @inheritDoc
  51. */
  52. get isCollapsed() {
  53. const length = this._ranges.length;
  54. return length === 0 ? true : super.isCollapsed;
  55. }
  56. /**
  57. * @inheritDoc
  58. */
  59. get anchor() {
  60. return super.anchor || this._getDefaultRange().start;
  61. }
  62. /**
  63. * @inheritDoc
  64. */
  65. get focus() {
  66. return super.focus || this._getDefaultRange().start;
  67. }
  68. /**
  69. * @inheritDoc
  70. */
  71. get rangeCount() {
  72. return this._ranges.length ? this._ranges.length : 1;
  73. }
  74. /**
  75. * Unbinds all events previously bound by document selection.
  76. */
  77. destroy() {
  78. for ( let i = 0; i < this._ranges.length; i++ ) {
  79. this._ranges[ i ].detach();
  80. }
  81. }
  82. /**
  83. * @inheritDoc
  84. */
  85. *getRanges() {
  86. if ( this._ranges.length ) {
  87. yield *super.getRanges();
  88. } else {
  89. yield this._getDefaultRange();
  90. }
  91. }
  92. /**
  93. * @inheritDoc
  94. */
  95. getFirstRange() {
  96. return super.getFirstRange() || this._getDefaultRange();
  97. }
  98. /**
  99. * @inheritDoc
  100. */
  101. removeAllRanges() {
  102. this.destroy();
  103. super.removeAllRanges();
  104. }
  105. /**
  106. * @inheritDoc
  107. */
  108. setRanges( newRanges, isLastBackward ) {
  109. this.destroy();
  110. super.setRanges( newRanges, isLastBackward );
  111. }
  112. /**
  113. * Removes all attributes from the selection.
  114. *
  115. * @fires engine.model.LiveSelection#change:attribute
  116. */
  117. clearAttributes() {
  118. this._attrs.clear();
  119. this._setStoredAttributesTo( new Map() );
  120. this.fire( 'change:attribute' );
  121. }
  122. /**
  123. * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
  124. *
  125. * @param {String} key Key of attribute to look for.
  126. * @returns {*} Attribute value or `undefined`.
  127. */
  128. getAttribute( key ) {
  129. return this._attrs.get( key );
  130. }
  131. /**
  132. * Returns iterator that iterates over this selection attributes.
  133. *
  134. * @returns {Iterable.<*>}
  135. */
  136. getAttributes() {
  137. return this._attrs[ Symbol.iterator ]();
  138. }
  139. /**
  140. * Checks if the selection has an attribute for given key.
  141. *
  142. * @param {String} key Key of attribute to check.
  143. * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
  144. */
  145. hasAttribute( key ) {
  146. return this._attrs.has( key );
  147. }
  148. /**
  149. * Removes an attribute with given key from the selection.
  150. *
  151. * @fires engine.model.LiveSelection#change:attribute
  152. * @param {String} key Key of attribute to remove.
  153. */
  154. removeAttribute( key ) {
  155. this._attrs.delete( key );
  156. this._removeStoredAttribute( key );
  157. this.fire( 'change:attribute' );
  158. }
  159. /**
  160. * Sets attribute on the selection. If attribute with the same key already is set, it overwrites its values.
  161. *
  162. * @fires engine.model.LiveSelection#change:attribute
  163. * @param {String} key Key of attribute to set.
  164. * @param {*} value Attribute value.
  165. */
  166. setAttribute( key, value ) {
  167. this._attrs.set( key, value );
  168. this._storeAttribute( key, value );
  169. this.fire( 'change:attribute' );
  170. }
  171. /**
  172. * Removes all attributes from the selection and sets given attributes.
  173. *
  174. * @fires engine.model.LiveSelection#change:attribute
  175. * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
  176. */
  177. setAttributesTo( attrs ) {
  178. this._attrs = toMap( attrs );
  179. this._setStoredAttributesTo( this._attrs );
  180. this.fire( 'change:attribute' );
  181. }
  182. /**
  183. * Creates and returns an instance of {@link engine.model.LiveSelection} that is a clone of given selection,
  184. * meaning that it has same ranges and same direction as it.
  185. *
  186. * @params {engine.model.Selection} otherSelection Selection to be cloned.
  187. * @returns {engine.model.LiveSelection} `LiveSelection` instance that is a clone of given selection.
  188. */
  189. /**
  190. * @inheritDoc
  191. */
  192. _popRange() {
  193. this._ranges.pop().detach();
  194. }
  195. /**
  196. * @inheritDoc
  197. */
  198. _pushRange( range ) {
  199. this._checkRange( range );
  200. this._ranges.push( LiveRange.createFromRange( range ) );
  201. }
  202. /**
  203. * Returns a default range for this selection. The default range is a collapsed range that starts and ends
  204. * at the beginning of this selection's document {@link engine.model.Document#_getDefaultRoot default root}.
  205. * This "artificial" range is important for algorithms that base on selection, so they won't break or need
  206. * special logic if there are no real ranges in the selection.
  207. *
  208. * @private
  209. * @returns {engine.model.Range}
  210. */
  211. _getDefaultRange() {
  212. const defaultRoot = this._document._getDefaultRoot();
  213. // Find the first position where the selection can be put.
  214. for ( let position of Range.createFromElement( defaultRoot ).getPositions() ) {
  215. if ( this._document.schema.check( { name: '$text', inside: position } ) ) {
  216. return new Range( position, position );
  217. }
  218. }
  219. const position = new Position( defaultRoot, [ 0 ] );
  220. return new Range( position, position );
  221. }
  222. /**
  223. * Iterates through all attributes stored in current selection's parent.
  224. *
  225. * @returns {Iterable.<*>}
  226. */
  227. *_getStoredAttributes() {
  228. const selectionParent = this.getFirstPosition().parent;
  229. if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
  230. for ( let attr of selectionParent.getAttributes() ) {
  231. if ( attr[ 0 ].indexOf( storePrefix ) === 0 ) {
  232. const realKey = attr[ 0 ].substr( storePrefix.length );
  233. yield [ realKey, attr[ 1 ] ];
  234. }
  235. }
  236. }
  237. }
  238. /**
  239. * Removes attribute with given key from attributes stored in current selection's parent node.
  240. *
  241. * @private
  242. * @param {String} key Key of attribute to remove.
  243. */
  244. _removeStoredAttribute( key ) {
  245. const selectionParent = this.getFirstPosition().parent;
  246. if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
  247. const storeKey = LiveSelection._getStoreAttributeKey( key );
  248. this._document.enqueueChanges( () => {
  249. this._document.batch().removeAttr( storeKey, selectionParent );
  250. } );
  251. }
  252. }
  253. /**
  254. * Stores given attribute key and value in current selection's parent node if the selection is collapsed and
  255. * the parent node is empty.
  256. *
  257. * @private
  258. * @param {String} key Key of attribute to set.
  259. * @param {*} value Attribute value.
  260. */
  261. _storeAttribute( key, value ) {
  262. const selectionParent = this.getFirstPosition().parent;
  263. if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
  264. const storeKey = LiveSelection._getStoreAttributeKey( key );
  265. this._document.enqueueChanges( () => {
  266. this._document.batch().setAttr( storeKey, value, selectionParent );
  267. } );
  268. }
  269. }
  270. /**
  271. * Sets selection attributes stored in current selection's parent node to given set of attributes.
  272. *
  273. * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
  274. * @private
  275. */
  276. _setStoredAttributesTo( attrs ) {
  277. const selectionParent = this.getFirstPosition().parent;
  278. if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
  279. this._document.enqueueChanges( () => {
  280. const batch = this._document.batch();
  281. for ( let attr of this._getStoredAttributes() ) {
  282. const storeKey = LiveSelection._getStoreAttributeKey( attr[ 0 ] );
  283. batch.removeAttr( storeKey, selectionParent );
  284. }
  285. for ( let attr of attrs ) {
  286. const storeKey = LiveSelection._getStoreAttributeKey( attr[ 0 ] );
  287. batch.setAttr( storeKey, attr[ 1 ], selectionParent );
  288. }
  289. } );
  290. }
  291. }
  292. /**
  293. * Updates this selection attributes according to it's ranges and the document.
  294. *
  295. * @fires engine.model.LiveSelection#change:attribute
  296. * @protected
  297. */
  298. _updateAttributes() {
  299. const position = this.getFirstPosition();
  300. const positionParent = position.parent;
  301. let attrs = null;
  302. if ( !this.isCollapsed ) {
  303. // 1. If selection is a range...
  304. const range = this.getFirstRange();
  305. // ...look for a first character node in that range and take attributes from it.
  306. for ( let item of range ) {
  307. // This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
  308. // It can be done better by using `break;` instead of checking `attrs === null`.
  309. if ( item.type == 'TEXT' && attrs === null ) {
  310. attrs = item.item.getAttributes();
  311. }
  312. }
  313. } else {
  314. // 2. If the selection is a caret or the range does not contain a character node...
  315. const nodeBefore = positionParent.getChild( position.offset - 1 );
  316. const nodeAfter = positionParent.getChild( position.offset );
  317. // ...look at the node before caret and take attributes from it if it is a character node.
  318. attrs = getAttrsIfCharacter( nodeBefore );
  319. // 3. If not, look at the node after caret...
  320. if ( !attrs ) {
  321. attrs = getAttrsIfCharacter( nodeAfter );
  322. }
  323. // 4. If not, try to find the first character on the left, that is in the same node.
  324. if ( !attrs ) {
  325. let node = nodeBefore;
  326. while ( node && !attrs ) {
  327. node = node.previousSibling;
  328. attrs = getAttrsIfCharacter( node );
  329. }
  330. }
  331. // 5. If not found, try to find the first character on the right, that is in the same node.
  332. if ( !attrs ) {
  333. let node = nodeAfter;
  334. while ( node && !attrs ) {
  335. node = node.nextSibling;
  336. attrs = getAttrsIfCharacter( node );
  337. }
  338. }
  339. // 6. If not found, selection should retrieve attributes from parent.
  340. if ( !attrs ) {
  341. attrs = this._getStoredAttributes();
  342. }
  343. }
  344. if ( attrs ) {
  345. this._attrs = new Map( attrs );
  346. } else {
  347. this.clearAttributes();
  348. }
  349. function getAttrsIfCharacter( node ) {
  350. if ( node instanceof CharacterProxy ) {
  351. return node.getAttributes();
  352. }
  353. return null;
  354. }
  355. this.fire( 'change:attribute' );
  356. }
  357. /**
  358. * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
  359. *
  360. * @param {String} key Attribute key to convert.
  361. * @returns {String} Converted attribute key, applicable for selection store.
  362. */
  363. static _getStoreAttributeKey( key ) {
  364. return storePrefix + key;
  365. }
  366. }
  367. /**
  368. * Fired whenever selection attributes are changed.
  369. *
  370. * @event engine.model.LiveSelection#change:attribute
  371. */