8
0

selection.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  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 Position from './position.js';
  7. import Range from './range.js';
  8. import LiveRange from './liverange.js';
  9. import EmitterMixin from '../../utils/emittermixin.js';
  10. import CharacterProxy from './characterproxy.js';
  11. import CKEditorError from '../../utils/ckeditorerror.js';
  12. import toMap from '../../utils/tomap.js';
  13. import mix from '../../utils/mix.js';
  14. const storePrefix = 'selection:';
  15. /**
  16. * Represents a selection that is made on nodes in {@link engine.treeModel.Document}. `Selection` instance is
  17. * created by {@link engine.treeModel.Document}. You should not need to create an instance of `Selection`.
  18. *
  19. * Keep in mind that selection always contains at least one range. If no ranges has been added to selection or all ranges
  20. * got removed from selection, the selection will be reset to contain {@link engine.treeModel.Selection#_getDefaultRange the default range}.
  21. *
  22. * @memberOf engine.treeModel
  23. */
  24. export default class Selection {
  25. /**
  26. * Creates an empty selection.
  27. *
  28. * @param {engine.treeModel.Document} document Document which owns this selection.
  29. */
  30. constructor( document ) {
  31. /**
  32. * List of attributes set on current selection.
  33. *
  34. * @protected
  35. * @member {Map} engine.treeModel.Selection#_attrs
  36. */
  37. this._attrs = new Map();
  38. /**
  39. * Document which owns this selection.
  40. *
  41. * @private
  42. * @member {engine.treeModel.Document} engine.treeModel.Selection#_document
  43. */
  44. this._document = document;
  45. /**
  46. * Specifies whether the last added range was added as a backward or forward range.
  47. *
  48. * @private
  49. * @member {Boolean} engine.treeModel.Selection#_lastRangeBackward
  50. */
  51. this._lastRangeBackward = false;
  52. /**
  53. * Stores all ranges that are selected.
  54. *
  55. * @private
  56. * @member {Array.<engine.treeModel.LiveRange>} engine.treeModel.Selection#_ranges
  57. */
  58. this._ranges = [];
  59. }
  60. /**
  61. * Selection anchor. Anchor may be described as a position where the selection starts. Together with
  62. * {@link engine.treeModel.Selection#focus} they define the direction of selection, which is important
  63. * when expanding/shrinking selection. Anchor is always the start or end of the most recent added range.
  64. * It may be a bit unintuitive when there are multiple ranges in selection.
  65. *
  66. * @see engine.treeModel.Selection#focus
  67. * @type {engine.treeModel.LivePosition}
  68. */
  69. get anchor() {
  70. let range = this._ranges.length ? this._ranges[ this._ranges.length - 1 ] : this._getDefaultRange();
  71. return this._lastRangeBackward ? range.end : range.start;
  72. }
  73. /**
  74. * Selection focus. Focus is a position where the selection ends.
  75. *
  76. * @see engine.treeModel.Selection#anchor
  77. * @type {engine.treeModel.LivePosition}
  78. */
  79. get focus() {
  80. let range = this._ranges.length ? this._ranges[ this._ranges.length - 1 ] : this._getDefaultRange();
  81. return this._lastRangeBackward ? range.start : range.end;
  82. }
  83. /**
  84. * Returns whether the selection is collapsed. Selection is collapsed when all it's ranges are collapsed.
  85. *
  86. * @type {Boolean}
  87. */
  88. get isCollapsed() {
  89. for ( let i = 0; i < this._ranges.length; i++ ) {
  90. if ( !this._ranges[ i ].isCollapsed ) {
  91. return false;
  92. }
  93. }
  94. return true;
  95. }
  96. /**
  97. * Specifies whether the {@link engine.treeModel.Selection#focus} precedes {@link engine.treeModel.Selection#anchor}.
  98. *
  99. * @type {Boolean}
  100. */
  101. get isBackward() {
  102. return !this.isCollapsed && this._lastRangeBackward;
  103. }
  104. /**
  105. * Adds a range to the selection. Added range is copied and converted to {@link engine.treeModel.LiveRange}. This means
  106. * that passed range is not saved in the Selection instance and you can safely operate on it.
  107. *
  108. * Accepts a flag describing in which way the selection is made - passed range might be selected from
  109. * {@link engine.treeModel.Range#start} to {@link engine.treeModel.Range#end} or from {@link engine.treeModel.Range#end}
  110. * to {@link engine.treeModel.Range#start}. The flag is used to set {@link engine.treeModel.Selection#anchor} and
  111. * {@link engine.treeModel.Selection#focus} properties.
  112. *
  113. * @fires engine.treeModel.Selection#change:range
  114. * @param {engine.treeModel.Range} range Range to add.
  115. * @param {Boolean} [isBackward] Flag describing if added range was selected forward - from start to end (`false`)
  116. * or backward - from end to start (`true`). Defaults to `false`.
  117. */
  118. addRange( range, isBackward ) {
  119. this._pushRange( range );
  120. this._lastRangeBackward = !!isBackward;
  121. this.fire( 'change:range' );
  122. }
  123. /**
  124. * Unbinds all events previously bound by this selection or objects created by this selection.
  125. */
  126. destroy() {
  127. for ( let i = 0; i < this._ranges.length; i++ ) {
  128. this._ranges[ i ].detach();
  129. }
  130. }
  131. /**
  132. * Returns an iterator that contains copies of all ranges added to the selection.
  133. *
  134. * @returns {Iterator.<engine.treeModel.Range>}
  135. */
  136. *getRanges() {
  137. if ( this._ranges.length ) {
  138. for ( let range of this._ranges ) {
  139. yield Range.createFromRange( range );
  140. }
  141. } else {
  142. yield this._getDefaultRange();
  143. }
  144. }
  145. /**
  146. * Returns the first range in the selection. First range is the one which {@link engine.treeModel.Range#start start} position
  147. * {@link engine.treeModel.Position#isBefore is before} start position of all other ranges (not to confuse with the first range
  148. * added to the selection).
  149. *
  150. * @returns {engine.treeModel.Range}
  151. */
  152. getFirstRange() {
  153. let first = null;
  154. for ( let i = 0; i < this._ranges.length; i++ ) {
  155. let range = this._ranges[ i ];
  156. if ( !first || range.start.isBefore( first.start ) ) {
  157. first = range;
  158. }
  159. }
  160. return first ? Range.createFromRange( first ) : this._getDefaultRange();
  161. }
  162. /**
  163. * Returns the first position in the selection. First position is the position that {@link engine.treeModel.Position#isBefore is before}
  164. * any other position in the selection ranges.
  165. *
  166. * @returns {engine.treeModel.Position}
  167. */
  168. getFirstPosition() {
  169. return Position.createFromPosition( this.getFirstRange().start );
  170. }
  171. /**
  172. * Removes all ranges that were added to the selection. Fires update event.
  173. *
  174. * @fires engine.treeModel.Selection#change:range
  175. */
  176. removeAllRanges() {
  177. this.destroy();
  178. this._ranges = [];
  179. this.fire( 'change:range' );
  180. }
  181. /**
  182. * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
  183. * is treated like the last added range and is used to set {@link #anchor} and {@link #focus}. Accepts a flag
  184. * describing in which way the selection is made (see {@link #addRange}).
  185. *
  186. * @fires engine.treeModel.Selection#change:range
  187. * @param {Array.<engine.treeModel.Range>} newRanges Array of ranges to set.
  188. * @param {Boolean} [isLastBackward] Flag describing if last added range was selected forward - from start to end (`false`)
  189. * or backward - from end to start (`true`). Defaults to `false`.
  190. */
  191. setRanges( newRanges, isLastBackward ) {
  192. this.destroy();
  193. this._ranges = [];
  194. for ( let i = 0; i < newRanges.length; i++ ) {
  195. this._pushRange( newRanges[ i ] );
  196. }
  197. this._lastRangeBackward = !!isLastBackward;
  198. this.fire( 'change:range' );
  199. }
  200. /**
  201. * Sets collapsed selection in the specified location.
  202. *
  203. * The location can be specified in the same form as {@link engine.treeModel.Position.createAt} parameters.
  204. *
  205. * @fires engine.treeModel.Selection#change:range
  206. * @param {engine.treeModel.Node|engine.treeModel.Position} nodeOrPosition
  207. * @param {Number|'END'|'BEFORE'|'AFTER'} [offset=0] Offset or one of the flags. Used only when
  208. * first parameter is a node.
  209. */
  210. collapse( nodeOrPosition, offset ) {
  211. const pos = Position.createAt( nodeOrPosition, offset );
  212. const range = new Range( pos, pos );
  213. this.setRanges( [ range ] );
  214. }
  215. /**
  216. * Sets {@link engine.treeModel.Selection#focus} in the specified location.
  217. *
  218. * The location can be specified in the same form as {@link engine.treeModel.Position.createAt} parameters.
  219. *
  220. * @fires engine.treeModel.Selection#change:range
  221. * @param {engine.treeModel.Node|engine.treeModel.Position} nodeOrPosition
  222. * @param {Number|'END'|'BEFORE'|'AFTER'} [offset=0] Offset or one of the flags. Used only when
  223. * first parameter is a node.
  224. */
  225. setFocus( nodeOrPosition, offset ) {
  226. const newFocus = Position.createAt( nodeOrPosition, offset );
  227. if ( newFocus.compareWith( this.focus ) == 'SAME' ) {
  228. return;
  229. }
  230. const anchor = this.anchor;
  231. if ( this._ranges.length ) {
  232. // TODO Replace with _popRange, so child classes can override this (needed for #329).
  233. this._ranges.pop().detach();
  234. }
  235. if ( newFocus.compareWith( anchor ) == 'BEFORE' ) {
  236. this.addRange( new Range( newFocus, anchor ), true );
  237. } else {
  238. this.addRange( new Range( anchor, newFocus ) );
  239. }
  240. }
  241. /**
  242. * Removes all attributes from the selection.
  243. *
  244. * @fires engine.treeModel.Selection#change:attribute
  245. */
  246. clearAttributes() {
  247. this._attrs.clear();
  248. this._setStoredAttributesTo( new Map() );
  249. this.fire( 'change:attribute' );
  250. }
  251. /**
  252. * Gets an attribute value for given key or undefined it that attribute is not set on selection.
  253. *
  254. * @param {String} key Key of attribute to look for.
  255. * @returns {*} Attribute value or null.
  256. */
  257. getAttribute( key ) {
  258. return this._attrs.get( key );
  259. }
  260. /**
  261. * Returns iterator that iterates over this selection attributes.
  262. *
  263. * @returns {Iterable.<*>}
  264. */
  265. getAttributes() {
  266. return this._attrs[ Symbol.iterator ]();
  267. }
  268. /**
  269. * Checks if the selection has an attribute for given key.
  270. *
  271. * @param {String} key Key of attribute to check.
  272. * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
  273. */
  274. hasAttribute( key ) {
  275. return this._attrs.has( key );
  276. }
  277. /**
  278. * Removes an attribute with given key from the selection.
  279. *
  280. * @fires engine.treeModel.Selection#change:attribute
  281. * @param {String} key Key of attribute to remove.
  282. */
  283. removeAttribute( key ) {
  284. this._attrs.delete( key );
  285. this._removeStoredAttribute( key );
  286. this.fire( 'change:attribute' );
  287. }
  288. /**
  289. * Sets attribute on the selection. If attribute with the same key already is set, it overwrites its values.
  290. *
  291. * @fires engine.treeModel.Selection#change:attribute
  292. * @param {String} key Key of attribute to set.
  293. * @param {*} value Attribute value.
  294. */
  295. setAttribute( key, value ) {
  296. this._attrs.set( key, value );
  297. this._storeAttribute( key, value );
  298. this.fire( 'change:attribute' );
  299. }
  300. /**
  301. * Removes all attributes from the selection and sets given attributes.
  302. *
  303. * @fires engine.treeModel.Selection#change:attribute
  304. * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
  305. */
  306. setAttributesTo( attrs ) {
  307. this._attrs = toMap( attrs );
  308. this._setStoredAttributesTo( this._attrs );
  309. this.fire( 'change:attribute' );
  310. }
  311. /**
  312. * Converts given range to {@link engine.treeModel.LiveRange} and adds it to internal ranges array. Throws an error
  313. * if given range is intersecting with any range that is already stored in this selection.
  314. *
  315. * @private
  316. * @param {engine.treeModel.Range} range Range to add.
  317. */
  318. _pushRange( range ) {
  319. for ( let i = 0; i < this._ranges.length ; i++ ) {
  320. if ( range.isIntersecting( this._ranges[ i ] ) ) {
  321. /**
  322. * Trying to add a range that intersects with another range from selection.
  323. *
  324. * @error selection-range-intersects
  325. * @param {engine.treeModel.Range} addedRange Range that was added to the selection.
  326. * @param {engine.treeModel.Range} intersectingRange Range from selection that intersects with `addedRange`.
  327. */
  328. throw new CKEditorError(
  329. 'selection-range-intersects: Trying to add a range that intersects with another range from selection.',
  330. { addedRange: range, intersectingRange: this._ranges[ i ] }
  331. );
  332. }
  333. }
  334. this._ranges.push( LiveRange.createFromRange( range ) );
  335. }
  336. /**
  337. * Iterates through all attributes stored in current selection's parent.
  338. *
  339. * @returns {Iterable.<*>}
  340. */
  341. *_getStoredAttributes() {
  342. const selectionParent = this.getFirstPosition().parent;
  343. if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
  344. for ( let attr of selectionParent.getAttributes() ) {
  345. if ( attr[ 0 ].indexOf( storePrefix ) === 0 ) {
  346. const realKey = attr[ 0 ].substr( storePrefix.length );
  347. yield [ realKey, attr[ 1 ] ];
  348. }
  349. }
  350. }
  351. }
  352. /**
  353. * Removes attribute with given key from attributes stored in current selection's parent node.
  354. *
  355. * @private
  356. * @param {String} key Key of attribute to remove.
  357. */
  358. _removeStoredAttribute( key ) {
  359. const selectionParent = this.getFirstPosition().parent;
  360. if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
  361. const storeKey = Selection._getStoreAttributeKey( key );
  362. this._document.enqueueChanges( () => {
  363. this._document.batch().removeAttr( storeKey, selectionParent );
  364. } );
  365. }
  366. }
  367. /**
  368. * Stores given attribute key and value in current selection's parent node if the selection is collapsed and
  369. * the parent node is empty.
  370. *
  371. * @private
  372. * @param {String} key Key of attribute to set.
  373. * @param {*} value Attribute value.
  374. */
  375. _storeAttribute( key, value ) {
  376. const selectionParent = this.getFirstPosition().parent;
  377. if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
  378. const storeKey = Selection._getStoreAttributeKey( key );
  379. this._document.enqueueChanges( () => {
  380. this._document.batch().setAttr( storeKey, value, selectionParent );
  381. } );
  382. }
  383. }
  384. /**
  385. * Sets selection attributes stored in current selection's parent node to given set of attributes.
  386. *
  387. * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
  388. * @private
  389. */
  390. _setStoredAttributesTo( attrs ) {
  391. const selectionParent = this.getFirstPosition().parent;
  392. if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
  393. this._document.enqueueChanges( () => {
  394. const batch = this._document.batch();
  395. for ( let attr of this._getStoredAttributes() ) {
  396. const storeKey = Selection._getStoreAttributeKey( attr[ 0 ] );
  397. batch.removeAttr( storeKey, selectionParent );
  398. }
  399. for ( let attr of attrs ) {
  400. const storeKey = Selection._getStoreAttributeKey( attr[ 0 ] );
  401. batch.setAttr( storeKey, attr[ 1 ], selectionParent );
  402. }
  403. } );
  404. }
  405. }
  406. /**
  407. * Updates this selection attributes based on it's position in the Tree Model.
  408. *
  409. * @protected
  410. */
  411. _updateAttributes() {
  412. const position = this.getFirstPosition();
  413. const positionParent = position.parent;
  414. let attrs = null;
  415. if ( this.isCollapsed === false ) {
  416. // 1. If selection is a range...
  417. const range = this.getFirstRange();
  418. // ...look for a first character node in that range and take attributes from it.
  419. for ( let item of range ) {
  420. if ( item.type == 'TEXT' ) {
  421. attrs = item.item.getAttributes();
  422. break;
  423. }
  424. }
  425. }
  426. // 2. If the selection is a caret or the range does not contain a character node...
  427. if ( !attrs && this.isCollapsed === true ) {
  428. const nodeBefore = positionParent.getChild( position.offset - 1 );
  429. const nodeAfter = positionParent.getChild( position.offset );
  430. // ...look at the node before caret and take attributes from it if it is a character node.
  431. attrs = getAttrsIfCharacter( nodeBefore );
  432. // 3. If not, look at the node after caret...
  433. if ( !attrs ) {
  434. attrs = getAttrsIfCharacter( nodeAfter );
  435. }
  436. // 4. If not, try to find the first character on the left, that is in the same node.
  437. if ( !attrs ) {
  438. let node = nodeBefore;
  439. while ( node && !attrs ) {
  440. node = node.previousSibling;
  441. attrs = getAttrsIfCharacter( node );
  442. }
  443. }
  444. // 5. If not found, try to find the first character on the right, that is in the same node.
  445. if ( !attrs ) {
  446. let node = nodeAfter;
  447. while ( node && !attrs ) {
  448. node = node.nextSibling;
  449. attrs = getAttrsIfCharacter( node );
  450. }
  451. }
  452. // 6. If not found, selection should retrieve attributes from parent.
  453. if ( !attrs ) {
  454. attrs = this._getStoredAttributes();
  455. }
  456. }
  457. if ( attrs ) {
  458. this._attrs = new Map( attrs );
  459. } else {
  460. this.clearAttributes();
  461. }
  462. function getAttrsIfCharacter( node ) {
  463. if ( node instanceof CharacterProxy ) {
  464. return node.getAttributes();
  465. }
  466. return null;
  467. }
  468. this.fire( 'change:attribute' );
  469. }
  470. /**
  471. * Returns a default range for this selection. The default range is a collapsed range that starts and ends
  472. * at the beginning of this selection's document {@link engine.treeModel.Document#_getDefaultRoot default root}.
  473. * This "artificial" range is important for algorithms that base on selection, so they won't break or need
  474. * special logic if there are no real ranges in the selection.
  475. *
  476. * @private
  477. * @returns {engine.treeModel.Range}
  478. */
  479. _getDefaultRange() {
  480. const defaultRoot = this._document._getDefaultRoot();
  481. const pos = new Position( defaultRoot, [ 0 ] );
  482. return new Range( pos, pos );
  483. }
  484. /**
  485. * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
  486. *
  487. * @param {String} key Attribute key to convert.
  488. * @returns {String} Converted attribute key, applicable for selection store.
  489. */
  490. static _getStoreAttributeKey( key ) {
  491. return storePrefix + key;
  492. }
  493. }
  494. mix( Selection, EmitterMixin );
  495. /**
  496. * Fired whenever selection ranges are changed through {@link engine.treeModel.Selection Selection API}. Not fired when
  497. * {@link engine.treeModel.LiveRange live ranges} inserted in selection change because of Tree Model changes.
  498. *
  499. * @event engine.treeModel.Selection#change:range
  500. */
  501. /**
  502. * Fired whenever selection attributes are changed.
  503. *
  504. * @event engine.treeModel.Selection#change:attribute
  505. */