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.
6 * @fileoverview Classes related to cursors that point to and select parts of
10 goog.provide('cursors.Cursor');
11 goog.provide('cursors.Movement');
12 goog.provide('cursors.Range');
13 goog.provide('cursors.Unit');
15 goog.require('AutomationUtil');
18 * The special index that represents a cursor pointing to a node without
19 * pointing to any part of its accessible text.
21 cursors.NODE_INDEX = -1;
24 * Represents units of CursorMovement.
28 /** A single character within accessible name or value. */
29 CHARACTER: 'character',
31 /** A range of characters (given by attributes on automation nodes). */
37 /** Formed by a set of leaf nodes that are inline. */
42 * Represents the ways in which cursors can move given a cursor unit.
46 /** Move to the beginning or end of the current unit. */
49 /** Move to the next unit in a particular direction. */
50 DIRECTIONAL: 'directional'
53 goog.scope(function() {
54 var AutomationNode = chrome.automation.AutomationNode;
55 var Dir = AutomationUtil.Dir;
56 var Movement = cursors.Movement;
57 var Role = chrome.automation.RoleType;
58 var Unit = cursors.Unit;
61 * Represents a position within the automation tree.
63 * @param {!AutomationNode} node
64 * @param {number} index A 0-based index into either this cursor's name or value
65 * attribute. Relies on the fact that a node has either a name or a value but
66 * not both. An index of |cursors.NODE_INDEX| means the node as a whole is
67 * pointed to and covers the case where the accessible text is empty.
69 cursors.Cursor = function(node, index) {
70 /** @type {!AutomationNode} @private */
72 /** @type {number} @private */
77 * Convenience method to construct a Cursor from a node.
78 * @param {!AutomationNode} node
79 * @return {!cursors.Cursor}
81 cursors.Cursor.fromNode = function(node) {
82 return new cursors.Cursor(node, cursors.NODE_INDEX);
85 cursors.Cursor.prototype = {
87 * Returns true if |rhs| is equal to this cursor.
88 * @param {!cursors.Cursor} rhs
91 equals: function(rhs) {
92 return this.node_ === rhs.getNode() &&
93 this.index_ === rhs.getIndex();
97 * @return {!AutomationNode}
106 getIndex: function() {
111 * Gets the accessible text of the node associated with this cursor.
113 * Note that only one of |name| or |value| attribute is ever nonempty on an
114 * automation node. If either contains whitespace, we still treat it as we do
115 * for a nonempty string.
116 * @param {!AutomationNode=} opt_node Use this node rather than this cursor's
120 getText: function(opt_node) {
121 var node = opt_node || this.node_;
122 return node.attributes.name || node.attributes.value || '';
126 * Makes a Cursor which has been moved from this cursor by the unit in the
127 * given direction using the given movement type.
129 * @param {Movement} movement
131 * @return {!cursors.Cursor} The moved cursor.
133 move: function(unit, movement, dir) {
134 var newNode = this.node_;
135 var newIndex = this.index_;
137 if (unit != Unit.NODE && newIndex === cursors.NODE_INDEX)
142 // BOUND and DIRECTIONAL are the same for characters.
143 newIndex = dir == Dir.FORWARD ? newIndex + 1 : newIndex - 1;
144 if (newIndex < 0 || newIndex >= this.getText().length) {
145 newNode = AutomationUtil.findNextNode(
146 newNode, dir, AutomationPredicate.leafWithText);
149 dir == Dir.FORWARD ? 0 : this.getText(newNode).length - 1;
150 newIndex = newIndex == -1 ? 0 : newIndex;
152 newIndex = this.index_;
159 if (newNode.role == Role.inlineTextBox) {
161 for (var i = 0; i < newNode.attributes.wordStarts.length; i++) {
162 if (newIndex >= newNode.attributes.wordStarts[i] &&
163 newIndex <= newNode.attributes.wordEnds[i]) {
164 start = newNode.attributes.wordStarts[i];
165 end = newNode.attributes.wordEnds[i];
169 if (goog.isDef(start) && goog.isDef(end))
170 newIndex = dir == Dir.FORWARD ? end : start;
172 // TODO(dtseng): Figure out what to do in this case.
175 case Movement.DIRECTIONAL:
176 if (newNode.role == Role.inlineTextBox) {
178 for (var i = 0; i < newNode.attributes.wordStarts.length; i++) {
179 if (newIndex >= newNode.attributes.wordStarts[i] &&
180 newIndex <= newNode.attributes.wordEnds[i]) {
181 var nextIndex = dir == Dir.FORWARD ? i + 1 : i - 1;
182 start = newNode.attributes.wordStarts[nextIndex];
183 end = newNode.attributes.wordEnds[nextIndex];
187 if (goog.isDef(start)) {
190 // The backward case is special at the beginning of nodes.
191 if (dir == Dir.BACKWARD && newIndex != 0) {
194 newNode = AutomationUtil.findNextNode(newNode, dir,
195 AutomationPredicate.leaf);
198 if (dir == Dir.BACKWARD &&
199 newNode.role == Role.inlineTextBox) {
200 var starts = newNode.attributes.wordStarts;
201 newIndex = starts[starts.length - 1] || 0;
203 // TODO(dtseng): Figure out what to do for general nodes.
209 // TODO(dtseng): Figure out what to do in this case.
216 newIndex = dir == Dir.FORWARD ? this.getText().length - 1 : 0;
218 case Movement.DIRECTIONAL:
219 newNode = AutomationUtil.findNextNode(
220 newNode, dir, AutomationPredicate.leaf) || this.node_;
221 newIndex = cursors.NODE_INDEX;
229 newNode = AutomationUtil.findNodeUntil(newNode, dir,
230 AutomationPredicate.linebreak, {before: true});
231 newNode = newNode || this.node_;
233 dir == Dir.FORWARD ? this.getText(newNode).length : 0;
235 case Movement.DIRECTIONAL:
236 newNode = AutomationUtil.findNodeUntil(
237 newNode, dir, AutomationPredicate.linebreak);
242 throw 'Unrecognized unit: ' + unit;
244 newNode = newNode || this.node_;
245 newIndex = goog.isDef(newIndex) ? newIndex : this.index_;
246 return new cursors.Cursor(newNode, newIndex);
251 * Represents a range in the automation tree. There is no visible selection on
252 * the page caused by usage of this object.
253 * It is assumed that the caller provides |start| and |end| in document order.
254 * @param {!cursors.Cursor} start
255 * @param {!cursors.Cursor} end
258 cursors.Range = function(start, end) {
259 /** @type {!cursors.Cursor} @private */
261 /** @type {!cursors.Cursor} @private */
266 * Convenience method to construct a Range surrounding one node.
267 * @param {!AutomationNode} node
268 * @return {!cursors.Range}
270 cursors.Range.fromNode = function(node) {
271 var cursor = cursors.Cursor.fromNode(node);
272 return new cursors.Range(cursor, cursor);
276 * Given |rangeA| and |rangeB| in order, determine which |Dir|
278 * @param {!cursors.Range} rangeA
279 * @param {!cursors.Range} rangeB
282 cursors.Range.getDirection = function(rangeA, rangeB) {
283 if (!rangeA || !rangeB)
286 // They are the same range.
287 if (rangeA.getStart().getNode() === rangeB.getStart().getNode() &&
288 rangeB.getEnd().getNode() === rangeA.getEnd().getNode())
292 AutomationUtil.getDirection(
293 rangeA.getStart().getNode(), rangeB.getEnd().getNode());
295 AutomationUtil.getDirection(
296 rangeB.getStart().getNode(), rangeA.getEnd().getNode());
298 // The two ranges are either partly overlapping or non overlapping.
299 if (testDirA == Dir.FORWARD && testDirB == Dir.BACKWARD)
301 else if (testDirA == Dir.BACKWARD && testDirB == Dir.FORWARD)
307 cursors.Range.prototype = {
309 * Returns true if |rhs| is equal to this range.
310 * @param {!cursors.Range} rhs
313 equals: function(rhs) {
314 return this.start_.equals(rhs.getStart()) &&
315 this.end_.equals(rhs.getEnd());
319 * Gets a cursor bounding this range.
320 * @param {Dir} dir Which endpoint cursor to return; Dir.FORWARD for end,
321 * Dir.BACKWARD for start.
322 * @param {boolean=} opt_reverse Specify to have Dir.BACKWARD return end,
323 * Dir.FORWARD return start.
324 * @return {!cursors.Cursor}
326 getBound: function(dir, opt_reverse) {
328 return dir == Dir.BACKWARD ? this.end_ : this.start_;
329 return dir == Dir.FORWARD ? this.end_ : this.start_;
333 * @return {!cursors.Cursor}
335 getStart: function() {
340 * @return {!cursors.Cursor}
347 * Returns true if this range covers less than a node.
350 isSubNode: function() {
351 return this.getStart().getNode() === this.getEnd().getNode() &&
352 this.getStart().getIndex() > -1 &&
353 this.getEnd().getIndex() > -1;
357 * Makes a Range which has been moved from this range by the given unit and
361 * @return {cursors.Range}
363 move: function(unit, dir) {
364 var newStart = this.start_;
365 var newEnd = newStart;
368 newStart = newStart.move(unit, Movement.BOUND, dir);
369 newEnd = newStart.move(unit, Movement.BOUND, Dir.FORWARD);
370 // Character crossed a node; collapses to the end of the node.
371 if (newStart.getNode() !== newEnd.getNode())
376 newStart = newStart.move(unit, Movement.DIRECTIONAL, dir);
377 newStart = newStart.move(unit, Movement.BOUND, Dir.BACKWARD);
378 newEnd = newStart.move(unit, Movement.BOUND, Dir.FORWARD);
381 newStart = newStart.move(unit, Movement.DIRECTIONAL, dir);
385 return new cursors.Range(newStart, newEnd);