selection.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import LiveRange from './liverange.js';
  7. import AttributeList from './attributelist.js';
  8. import EmitterMixin from '../emittermixin.js';
  9. import CKEditorError from '../ckeditorerror.js';
  10. import objectUtils from '../lib/lodash/object.js';
  11. /**
  12. * Represents a selection that is made on nodes in {@link treeModel.Document}. Selection instance is
  13. * created by {@link treeModel.Document}. In most scenarios you should not need to create an instance of Selection.
  14. *
  15. * @class treeModel.Selection
  16. */
  17. export default class Selection {
  18. /**
  19. * Creates an empty selection.
  20. *
  21. * @constructor
  22. */
  23. constructor() {
  24. /**
  25. * List of attributes set on current selection.
  26. *
  27. * @private
  28. * @property {treeModel.AttributeList} _attrs
  29. */
  30. this._attrs = new AttributeList();
  31. /**
  32. * Stores all ranges that are selected.
  33. *
  34. * @private
  35. * @property {Array.<LiveRange>}
  36. */
  37. this._ranges = [];
  38. /**
  39. * Specifies whether the last added range was added as a backward or forward range.
  40. *
  41. * @private
  42. * @property {Boolean}
  43. */
  44. this._lastRangeBackward = false;
  45. }
  46. /**
  47. * Selection anchor. Anchor may be described as a position where the selection starts.
  48. * Together with {@link #focus} they define the direction of selection, which is important
  49. * when expanding/shrinking selection. When there are no ranges in selection anchor is null.
  50. * Anchor is always a start or end of the most recent added range. It may be a bit unintuitive when
  51. * there are multiple ranges in selection.
  52. *
  53. * @property {treeModel.LivePosition|null}
  54. */
  55. get anchor() {
  56. if ( this._ranges.length > 0 ) {
  57. let range = this._ranges[ this._ranges.length - 1 ];
  58. return this._lastRangeBackward ? range.end : range.start;
  59. }
  60. return null;
  61. }
  62. /**
  63. * Selection focus. Focus is a position where the selection ends. When there are no ranges in selection,
  64. * focus is null.
  65. *
  66. * @see {#anchor}
  67. * @property {treeModel.LivePosition|null}
  68. */
  69. get focus() {
  70. if ( this._ranges.length > 0 ) {
  71. let range = this._ranges[ this._ranges.length - 1 ];
  72. return this._lastRangeBackward ? range.start : range.end;
  73. }
  74. return null;
  75. }
  76. /**
  77. * Returns whether the selection is collapsed. Selection is collapsed when all it's ranges are collapsed.
  78. *
  79. * @property {Boolean}
  80. */
  81. get isCollapsed() {
  82. for ( let i = 0; i < this._ranges.length; i++ ) {
  83. if ( !this._ranges[ i ].isCollapsed ) {
  84. return false;
  85. }
  86. }
  87. return true;
  88. }
  89. /**
  90. * Adds a range to the selection. Added range is copied and converted to {@link treeModel.LiveRange}. This means
  91. * that passed range is not saved in the Selection instance and you can safely operate on it. Accepts a flag
  92. * describing in which way the selection is made - passed range might be selected from {@link treeModel.Range#start}
  93. * to {@link treeModel.Range#end} or from {@link treeModel.Range#start} to {@link treeModel.Range#end}. The flag
  94. * is used to set {@link #anchor} and {@link #focus} properties.
  95. *
  96. * @param {treeModel.Range} range Range to add.
  97. * @param {Boolean} [isBackward] Flag describing if added range was selected forward - from start to end (`false`)
  98. * or backward - from end to start (`true`). Defaults to `false`.
  99. */
  100. addRange( range, isBackward ) {
  101. pushRange.call( this, range );
  102. this._lastRangeBackward = !!isBackward;
  103. this.fire( 'update' );
  104. }
  105. /**
  106. * Unbinds all events previously bound by this selection and objects created by this selection.
  107. */
  108. detach() {
  109. for ( let i = 0; i < this._ranges.length; i++ ) {
  110. this._ranges[ i ].detach();
  111. }
  112. }
  113. /**
  114. * @see {@link treeModel.AttributeList#getAttr}
  115. */
  116. getAttr( key ) {
  117. return this._attrs.getAttr( key );
  118. }
  119. /**
  120. * @see {@link treeModel.AttributeList#getAttrs}
  121. */
  122. getAttrs() {
  123. return this._attrs.getAttrs();
  124. }
  125. /**
  126. * Returns an array of ranges added to the selection. The method returns a copy of internal array, so
  127. * it will not change when ranges get added or removed from selection.
  128. *
  129. * @returns {Array.<LiveRange>}
  130. */
  131. getRanges() {
  132. return this._ranges.slice();
  133. }
  134. /**
  135. * @see {@link treeModel.AttributeList#hasAttr}
  136. */
  137. hasAttr( key ) {
  138. return this._attrs.hasAttr( key );
  139. }
  140. /**
  141. * @see {@link treeModel.AttributeList#removeAttr}
  142. */
  143. removeAttr( key ) {
  144. this._attrs.removeAttr( key );
  145. }
  146. /**
  147. * Removes all ranges that were added to the selection. Fires update event.
  148. */
  149. removeAllRanges() {
  150. this.detach();
  151. this._ranges = [];
  152. this.fire( 'update' );
  153. }
  154. /**
  155. * @see {@link treeModel.AttributeList#setAttr}
  156. */
  157. setAttr( attr ) {
  158. this._attrs.setAttr( attr );
  159. }
  160. /**
  161. * @see {@link treeModel.AttributeList#setAttrsTo}
  162. */
  163. setAttrsTo( attrs ) {
  164. this._attrs.setAttrsTo( attrs );
  165. }
  166. /**
  167. * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
  168. * is treated like the last added range and is used to set {@link #anchor} and {@link #focus}. Accepts a flag
  169. * describing in which way the selection is made (see {@link #addRange}).
  170. *
  171. * @param {Array.<treeModel.Range>} newRanges Array of ranges to set.
  172. * @param {Boolean} [isLastBackward] Flag describing if last added range was selected forward - from start to end (`false`)
  173. * or backward - from end to start (`true`). Defaults to `false`.
  174. */
  175. setRanges( newRanges, isLastBackward ) {
  176. this.detach();
  177. this._ranges = [];
  178. for ( let i = 0; i < newRanges.length; i++ ) {
  179. pushRange.call( this, newRanges[ i ] );
  180. }
  181. this._lastRangeBackward = !!isLastBackward;
  182. this.fire( 'update' );
  183. }
  184. }
  185. /**
  186. * Converts given range to {@link treeModel.LiveRange} and adds it to internal ranges array. Throws an error
  187. * if given range is intersecting with any range that is already stored in this selection.
  188. *
  189. * @private
  190. * @method pushRange
  191. * @memberOf {treeModel.Selection}
  192. * @param {treeModel.Range} range Range to add.
  193. */
  194. function pushRange( range ) {
  195. /* jshint validthis: true */
  196. for ( let i = 0; i < this._ranges.length ; i++ ) {
  197. if ( range.isIntersecting( this._ranges[ i ] ) ) {
  198. /**
  199. * Trying to add a range that intersects with another range from selection.
  200. *
  201. * @error selection-range-intersects
  202. * @param {treeModel.Range} addedRange Range that was added to the selection.
  203. * @param {treeModel.Range} intersectingRange Range from selection that intersects with `addedRange`.
  204. */
  205. throw new CKEditorError(
  206. 'selection-range-intersects: Trying to add a range that intersects with another range from selection.',
  207. { addedRange: range, intersectingRange: this._ranges[ i ] }
  208. );
  209. }
  210. }
  211. this._ranges.push( LiveRange.createFromRange( range ) );
  212. }
  213. objectUtils.extend( Selection.prototype, EmitterMixin );