8
0

selection.js 16 KB

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