selection.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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 '../emittermixin.js';
  10. import CKEditorError from '../ckeditorerror.js';
  11. import utils from '../utils.js';
  12. /**
  13. * Represents a selection that is made on nodes in {@link core.treeModel.Document}. Selection instance is
  14. * created by {@link core.treeModel.Document}. In most scenarios you should not need to create an instance of Selection.
  15. *
  16. * @memberOf core.treeModel
  17. */
  18. export default class Selection {
  19. /**
  20. * Creates an empty selection.
  21. */
  22. constructor() {
  23. /**
  24. * List of attributes set on current selection.
  25. *
  26. * @protected
  27. * @member {Map} core.treeModel.Selection#_attrs
  28. */
  29. this._attrs = new Map();
  30. /**
  31. * Stores all ranges that are selected.
  32. *
  33. * @private
  34. * @member {Array.<core.treeModel.LiveRange>} core.treeModel.Selection#_ranges
  35. */
  36. this._ranges = [];
  37. /**
  38. * Specifies whether the last added range was added as a backward or forward range.
  39. *
  40. * @private
  41. * @member {Boolean} core.treeModel.Selection#_lastRangeBackward
  42. */
  43. this._lastRangeBackward = false;
  44. }
  45. /**
  46. * Selection anchor. Anchor may be described as a position where the selection starts.
  47. * Together with {@link core.treeModel.Selection#focus} they define the direction of selection, which is important
  48. * when expanding/shrinking selection. When there are no ranges in selection anchor is null. Anchor is always
  49. * the start or end of the most recent added range. It may be a bit unintuitive when there are multiple ranges in selection.
  50. *
  51. * @see core.treeModel.Selection#focus
  52. * @type {core.treeModel.LivePosition|null}
  53. */
  54. get anchor() {
  55. if ( this.hasAnyRange ) {
  56. let range = this._ranges[ this._ranges.length - 1 ];
  57. return this._lastRangeBackward ? range.end : range.start;
  58. }
  59. return null;
  60. }
  61. /**
  62. * Selection focus. Focus is a position where the selection ends. When there are no ranges in selection, focus is null.
  63. *
  64. * @see core.treeModel.Selection#anchor
  65. * @type {core.treeModel.LivePosition|null}
  66. */
  67. get focus() {
  68. if ( this.hasAnyRange ) {
  69. let range = this._ranges[ this._ranges.length - 1 ];
  70. return this._lastRangeBackward ? range.start : range.end;
  71. }
  72. return null;
  73. }
  74. /**
  75. * Flag indicating whether the selection has any range in it.
  76. *
  77. * @readonly
  78. * @type {Boolean}
  79. */
  80. get hasAnyRange() {
  81. return this._ranges.length > 0;
  82. }
  83. /**
  84. * Returns whether the selection is collapsed. Selection is collapsed when all it's ranges are collapsed.
  85. * If selection has no ranges, returns null instead.
  86. *
  87. * @type {Boolean}
  88. */
  89. get isCollapsed() {
  90. if ( !this.hasAnyRange ) {
  91. return null;
  92. }
  93. for ( let i = 0; i < this._ranges.length; i++ ) {
  94. if ( !this._ranges[ i ].isCollapsed ) {
  95. return false;
  96. }
  97. }
  98. return true;
  99. }
  100. /**
  101. * Adds a range to the selection. Added range is copied and converted to {@link core.treeModel.LiveRange}. This means
  102. * that passed range is not saved in the Selection instance and you can safely operate on it.
  103. *
  104. * Accepts a flag describing in which way the selection is made - passed range might be selected from
  105. * {@link core.treeModel.Range#start} to {@link core.treeModel.Range#end} or from {@link core.treeModel.Range#start}
  106. * to {@link core.treeModel.Range#end}. The flag is used to set {@link core.treeModel.Selection#anchor} and
  107. * {@link core.treeModel.Selection#focus} properties.
  108. *
  109. * @fires {@link core.treeModel.Selection.update update}
  110. * @param {core.treeModel.Range} range Range to add.
  111. * @param {Boolean} [isBackward] Flag describing if added range was selected forward - from start to end (`false`)
  112. * or backward - from end to start (`true`). Defaults to `false`.
  113. */
  114. addRange( range, isBackward ) {
  115. pushRange.call( this, range );
  116. this._lastRangeBackward = !!isBackward;
  117. this.fire( 'update' );
  118. }
  119. /**
  120. * Unbinds all events previously bound by this selection or objects created by this selection.
  121. */
  122. detach() {
  123. for ( let i = 0; i < this._ranges.length; i++ ) {
  124. this._ranges[ i ].detach();
  125. }
  126. }
  127. /**
  128. * Returns an array of ranges added to the selection. The method returns a copy of internal array, so
  129. * it will not change when ranges get added or removed from selection.
  130. *
  131. * @returns {Array.<LiveRange>}
  132. */
  133. getRanges() {
  134. return this._ranges.slice();
  135. }
  136. /**
  137. * Returns the first range in the selection. First range is the one which {@link core.treeModel.Range#start start} position
  138. * {@link core.treeModel.Position#isBefore is before} start position of all other ranges (not to confuse with the first range
  139. * added to the selection).
  140. *
  141. * If there are no ranges in selection, retruns null instead.
  142. *
  143. * @returns {core.treeModel.Range|null}
  144. */
  145. getFirstRange() {
  146. let first = null;
  147. for ( let i = 0; i < this._ranges.length; i++ ) {
  148. let range = this._ranges[ i ];
  149. if ( !first || range.start.isBefore( first.start ) ) {
  150. first = range;
  151. }
  152. }
  153. return first && Range.createFromRange( first );
  154. }
  155. /**
  156. * Returns the first position in the selection. First position is the position that {@link core.treeModel.Position#isBefore is before}
  157. * any other position in the selection ranges.
  158. *
  159. * @returns {core.treeModel.Position|null}
  160. */
  161. getFirstPosition() {
  162. let firstRange = this.getFirstRange();
  163. return firstRange && Position.createFromPosition( firstRange.start );
  164. }
  165. /**
  166. * Removes all ranges that were added to the selection. Fires update event.
  167. *
  168. * @fires {@link core.treeModel.Selection.update update}
  169. */
  170. removeAllRanges() {
  171. this.detach();
  172. this._ranges = [];
  173. this.fire( 'update' );
  174. }
  175. /**
  176. * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
  177. * is treated like the last added range and is used to set {@link #anchor} and {@link #focus}. Accepts a flag
  178. * describing in which way the selection is made (see {@link #addRange}).
  179. *
  180. * @fires {@link core.treeModel.Selection.update update}
  181. * @param {Array.<core.treeModel.Range>} newRanges Array of ranges to set.
  182. * @param {Boolean} [isLastBackward] Flag describing if last added range was selected forward - from start to end (`false`)
  183. * or backward - from end to start (`true`). Defaults to `false`.
  184. */
  185. setRanges( newRanges, isLastBackward ) {
  186. this.detach();
  187. this._ranges = [];
  188. for ( let i = 0; i < newRanges.length; i++ ) {
  189. pushRange.call( this, newRanges[ i ] );
  190. }
  191. this._lastRangeBackward = !!isLastBackward;
  192. this.fire( 'update' );
  193. }
  194. /**
  195. * Checks if the selection has an attribute for given key.
  196. *
  197. * @param {String} key Key of attribute to check.
  198. * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
  199. */
  200. hasAttribute( key ) {
  201. return this._attrs.has( key );
  202. }
  203. /**
  204. * Gets an attribute value for given key or undefined it that attribute is not set on selection.
  205. *
  206. * @param {String} key Key of attribute to look for.
  207. * @returns {*} Attribute value or null.
  208. */
  209. getAttribute( key ) {
  210. return this._attrs.get( key );
  211. }
  212. /**
  213. * Returns iterator that iterates over this selection attributes.
  214. *
  215. * @returns {Iterable.<*>}
  216. */
  217. getAttributes() {
  218. return this._attrs[ Symbol.iterator ]();
  219. }
  220. /**
  221. * Sets attribute on the selection. If attribute with the same key already is set, it overwrites its values.
  222. *
  223. * @param {String} key Key of attribute to set.
  224. * @param {*} value Attribute value.
  225. */
  226. setAttribute( key, value ) {
  227. this._attrs.set( key, value );
  228. }
  229. /**
  230. * Removes all attributes from the selection and sets given attributes.
  231. *
  232. * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
  233. */
  234. setAttributesTo( attrs ) {
  235. this._attrs = utils.toMap( attrs );
  236. }
  237. /**
  238. * Removes an attribute with given key from the selection.
  239. *
  240. * @param {String} key Key of attribute to remove.
  241. * @returns {Boolean} `true` if the attribute was set on the selection, `false` otherwise.
  242. */
  243. removeAttribute( key ) {
  244. return this._attrs.delete( key );
  245. }
  246. /**
  247. * Removes all attributes from the selection.
  248. */
  249. clearAttributes() {
  250. this._attrs.clear();
  251. }
  252. }
  253. /**
  254. * Converts given range to {@link core.treeModel.LiveRange} and adds it to internal ranges array. Throws an error
  255. * if given range is intersecting with any range that is already stored in this selection.
  256. *
  257. * @private
  258. * @method pushRange
  259. * @memberOf {core.treeModel.Selection}
  260. * @param {core.treeModel.Range} range Range to add.
  261. */
  262. function pushRange( range ) {
  263. /* jshint validthis: true */
  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. utils.mix( Selection, EmitterMixin );
  282. /**
  283. * Fired whenever selection ranges are changed through {@link core.treeModel.Selection Selection API}. Not fired when
  284. * {@link core.treeModel.LiveRange live ranges} inserted in selection change because of Tree Model changes.
  285. *
  286. * @event core.treeModel.Selection.update
  287. */