cursor_selection.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview Simple class to represent a cursor selection.
7 * A cursor selection is just two cursors; one for the start and one for
8 * the end of some interval in the document.
9 */
10
11goog.provide('cvox.CursorSelection');
12
13goog.require('cvox.Cursor');
14goog.require('cvox.SelectionUtil');
15goog.require('cvox.TraverseUtil');
16
17
18/**
19 * If the start node and end node are the same, and the indexes are the same,
20 * the selection is interpreted to be a node. Otherwise, it is interpreted
21 * to be a range.
22 * @param {!cvox.Cursor} start The starting cursor.
23 * @param {!cvox.Cursor} end The ending cursor.
24 * @param {boolean=} opt_reverse Whether to make it a reversed selection or
25 * not. Default is selection is not reversed. If start and end are in the
26 * wrong order, they will be swapped automatically.
27 * NOTE: Can't infer automatically whether the selection is reversed because
28 * for a selection on a single node, the start and end are equal.
29 * @constructor
30 */
31cvox.CursorSelection = function(start, end, opt_reverse) {
32  this.start = start.clone();
33  this.end = end.clone();
34
35  if (opt_reverse == undefined) {
36    opt_reverse = false;
37  }
38  /** @private */
39  this.isReversed_ = opt_reverse;
40
41  if ((this.isReversed_ &&
42       this.start.node.compareDocumentPosition(this.end.node) ==
43       cvox.CursorSelection.BEFORE) ||
44      (!this.isReversed_ &&
45       this.end.node.compareDocumentPosition(this.start.node) ==
46       cvox.CursorSelection.BEFORE)) {
47    var oldStart = this.start;
48    this.start = this.end;
49    this.end = oldStart;
50  }
51};
52
53
54/**
55 * From http://www.w3schools.com/jsref/met_node_comparedocumentposition.asp
56 */
57cvox.CursorSelection.BEFORE = 4;
58
59
60/**
61 * If true, ensures that this selection is reversed. Otherwise, ensures that
62 * it is not reversed.
63 * @param {boolean} reversed True to reverse. False to nonreverse.
64 * @return {!cvox.CursorSelection} For chaining.
65 */
66cvox.CursorSelection.prototype.setReversed = function(reversed) {
67  if (reversed == this.isReversed_) {
68    return this;
69  }
70  var oldStart = this.start;
71  this.start = this.end;
72  this.end = oldStart;
73  this.isReversed_ = reversed;
74  return this;
75};
76
77
78/**
79 * Returns true if this selection is a reverse selection.
80 * @return {boolean} true if reversed.
81 */
82cvox.CursorSelection.prototype.isReversed = function() {
83  return this.isReversed_;
84};
85
86
87/**
88 * Returns start if not reversed, end if reversed.
89 * @return {!cvox.Cursor} start if not reversed, end if reversed.
90 */
91cvox.CursorSelection.prototype.absStart = function() {
92  return this.isReversed_ ? this.end : this.start;
93};
94
95/**
96 * Returns end if not reversed, start if reversed.
97 * @return {!cvox.Cursor} end if not reversed, start if reversed.
98 */
99cvox.CursorSelection.prototype.absEnd = function() {
100  return this.isReversed_ ? this.start : this.end;
101};
102
103
104/**
105 * Clones the selection.
106 * @return {!cvox.CursorSelection} The cloned selection.
107 */
108cvox.CursorSelection.prototype.clone = function() {
109  return new cvox.CursorSelection(this.start, this.end, this.isReversed_);
110};
111
112
113/**
114 * Places a DOM selection around this CursorSelection.
115 */
116cvox.CursorSelection.prototype.select = function() {
117  var sel = window.getSelection();
118  sel.removeAllRanges();
119  this.normalize();
120  sel.addRange(this.getRange());
121};
122
123
124/**
125 * Creates a new cursor selection that starts and ends at the node.
126 * Returns null if node is null.
127 * @param {Node} node The node.
128 * @return {cvox.CursorSelection} The selection.
129 */
130cvox.CursorSelection.fromNode = function(node) {
131  if (!node) {
132    return null;
133  }
134  var text = cvox.TraverseUtil.getNodeText(node);
135
136  return new cvox.CursorSelection(
137      new cvox.Cursor(node, 0, text),
138      new cvox.Cursor(node, 0, text));
139};
140
141
142/**
143 * Creates a new cursor selection that starts and ends at document.body.
144 * @return {!cvox.CursorSelection} The selection.
145 */
146cvox.CursorSelection.fromBody = function() {
147    return /** @type {!cvox.CursorSelection} */ (
148        cvox.CursorSelection.fromNode(document.body));
149};
150
151/**
152 * Returns the text that the selection spans.
153 * @return {string} Text within the selection. '' if it is a node selection.
154 */
155cvox.CursorSelection.prototype.getText = function() {
156  if (this.start.equals(this.end)) {
157    return cvox.TraverseUtil.getNodeText(this.start.node);
158  }
159  return cvox.SelectionUtil.getRangeText(this.getRange());
160};
161
162/**
163 * Returns a range from the given selection.
164 * @return {Range} The range.
165 */
166cvox.CursorSelection.prototype.getRange = function() {
167  var range = document.createRange();
168  if (this.isReversed_) {
169    range.setStart(this.end.node, this.end.index);
170    range.setEnd(this.start.node, this.start.index);
171  } else {
172    range.setStart(this.start.node, this.start.index);
173    range.setEnd(this.end.node, this.end.index);
174  }
175  return range;
176};
177
178/**
179 * Check for equality.
180 * @param {!cvox.CursorSelection} rhs The CursorSelection to compare against.
181 * @return {boolean} True if equal.
182 */
183cvox.CursorSelection.prototype.equals = function(rhs) {
184  return this.start.equals(rhs.start) && this.end.equals(rhs.end);
185};
186
187/**
188 * Check for equality regardless of direction.
189 * @param {!cvox.CursorSelection} rhs The CursorSelection to compare against.
190 * @return {boolean} True if equal.
191 */
192cvox.CursorSelection.prototype.absEquals = function(rhs) {
193  return ((this.start.equals(rhs.start) && this.end.equals(rhs.end)) ||
194      (this.end.equals(rhs.start) && this.start.equals(rhs.end)));
195};
196
197/**
198 * Determines if this starts before another CursorSelection in document order.
199 * If this is reversed, then a reversed document order is checked.
200 * In the case that this and rhs start at the same position, we return true.
201 * @param {!cvox.CursorSelection} rhs The selection to compare.
202 * @return {boolean} True if this is before rhs.
203 */
204cvox.CursorSelection.prototype.directedBefore = function(rhs) {
205  var leftToRight = this.start.node.compareDocumentPosition(rhs.start.node) ==
206      cvox.CursorSelection.BEFORE;
207  return this.start.node == rhs.start.node ||
208      (this.isReversed() ? !leftToRight : leftToRight);
209};
210/**
211 * Normalizes this selection.
212 * Use this routine to adjust CursorSelection's that have been collapsed due to
213 * convention such as when a CursorSelection references a node without attention
214 * to its endpoints.
215 * The result is to surround the node with this cursor.
216 * @return {!cvox.CursorSelection} The normalized selection.
217 */
218cvox.CursorSelection.prototype.normalize = function() {
219  if (this.absEnd().index == 0 && this.absEnd().node) {
220    var node = this.absEnd().node;
221
222    // DOM ranges use different conventions when surrounding a node. For
223    // instance, input nodes endOffset is always 0 while h1's endOffset is 1
224    //with both having no children. Use a range to compute the endOffset.
225    var testRange = document.createRange();
226    testRange.selectNodeContents(node);
227    this.absEnd().index = testRange.endOffset;
228  }
229  return this;
230};
231
232/**
233 * Collapses to the directed start of the selection.
234 * @return {!cvox.CursorSelection} For chaining.
235 */
236cvox.CursorSelection.prototype.collapse = function() {
237  // Not a selection.
238  if (this.start.equals(this.end)) {
239    return this;
240  }
241  this.end.copyFrom(this.start);
242  if (this.start.text.length == 0) {
243    return this;
244  }
245  if (this.isReversed()) {
246    if (this.end.index > 0) {
247      this.end.index--;
248    }
249  } else {
250    if (this.end.index < this.end.text.length) {
251      this.end.index++;
252    }
253  }
254  return this;
255};
256