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 /** A leaf DOM-node. */
40 /** Formed by a set of leaf nodes that are inline. */
45 * Represents the ways in which cursors can move given a cursor unit.
49 /** Move to the beginning or end of the current unit. */
52 /** Move to the next unit in a particular direction. */
53 DIRECTIONAL: 'directional'
56 goog.scope(function() {
57 var AutomationNode = chrome.automation.AutomationNode;
58 var Dir = AutomationUtil.Dir;
59 var Movement = cursors.Movement;
60 var Role = chrome.automation.RoleType;
61 var Unit = cursors.Unit;
64 * Represents a position within the automation tree.
66 * @param {!AutomationNode} node
67 * @param {number} index A 0-based index into either this cursor's name or value
68 * attribute. Relies on the fact that a node has either a name or a value but
69 * not both. An index of |cursors.NODE_INDEX| means the node as a whole is
70 * pointed to and covers the case where the accessible text is empty.
72 cursors.Cursor = function(node, index) {
73 /** @type {!AutomationNode} @private */
75 /** @type {number} @private */
80 * Convenience method to construct a Cursor from a node.
81 * @param {!AutomationNode} node
82 * @return {!cursors.Cursor}
84 cursors.Cursor.fromNode = function(node) {
85 return new cursors.Cursor(node, cursors.NODE_INDEX);
88 cursors.Cursor.prototype = {
90 * Returns true if |rhs| is equal to this cursor.
91 * @param {!cursors.Cursor} rhs
94 equals: function(rhs) {
95 return this.node_ === rhs.node &&
96 this.index_ === rhs.getIndex();
100 * @return {!AutomationNode}
109 getIndex: function() {
114 * Gets the accessible text of the node associated with this cursor.
116 * Note that only one of |name| or |value| attribute is ever nonempty on an
117 * automation node. If either contains whitespace, we still treat it as we do
118 * for a nonempty string.
119 * @param {!AutomationNode=} opt_node Use this node rather than this cursor's
123 getText: function(opt_node) {
124 var node = opt_node || this.node_;
125 return node.name || node.value || '';
129 * Makes a Cursor which has been moved from this cursor by the unit in the
130 * given direction using the given movement type.
132 * @param {Movement} movement
134 * @return {!cursors.Cursor} The moved cursor.
136 move: function(unit, movement, dir) {
137 var newNode = this.node_;
138 var newIndex = this.index_;
140 if ((unit != Unit.NODE || unit != Unit.DOM_NODE) &&
141 newIndex === cursors.NODE_INDEX)
146 // BOUND and DIRECTIONAL are the same for characters.
147 newIndex = dir == Dir.FORWARD ? newIndex + 1 : newIndex - 1;
148 if (newIndex < 0 || newIndex >= this.getText().length) {
149 newNode = AutomationUtil.findNextNode(
150 newNode, dir, AutomationPredicate.leafWithText);
153 dir == Dir.FORWARD ? 0 : this.getText(newNode).length - 1;
154 newIndex = newIndex == -1 ? 0 : newIndex;
156 newIndex = this.index_;
163 if (newNode.role == Role.inlineTextBox) {
165 for (var i = 0; i < newNode.wordStarts.length; i++) {
166 if (newIndex >= newNode.wordStarts[i] &&
167 newIndex <= newNode.wordEnds[i]) {
168 start = newNode.wordStarts[i];
169 end = newNode.wordEnds[i];
173 if (goog.isDef(start) && goog.isDef(end))
174 newIndex = dir == Dir.FORWARD ? end : start;
176 // TODO(dtseng): Figure out what to do in this case.
179 case Movement.DIRECTIONAL:
180 if (newNode.role == Role.inlineTextBox) {
182 for (var i = 0; i < newNode.wordStarts.length; i++) {
183 if (newIndex >= newNode.wordStarts[i] &&
184 newIndex <= newNode.wordEnds[i]) {
185 var nextIndex = dir == Dir.FORWARD ? i + 1 : i - 1;
186 start = newNode.wordStarts[nextIndex];
187 end = newNode.wordEnds[nextIndex];
191 if (goog.isDef(start)) {
194 // The backward case is special at the beginning of nodes.
195 if (dir == Dir.BACKWARD && newIndex != 0) {
198 newNode = AutomationUtil.findNextNode(newNode, dir,
199 AutomationPredicate.leaf);
202 if (dir == Dir.BACKWARD &&
203 newNode.role == Role.inlineTextBox) {
204 var starts = newNode.wordStarts;
205 newIndex = starts[starts.length - 1] || 0;
207 // TODO(dtseng): Figure out what to do for general nodes.
213 // TODO(dtseng): Figure out what to do in this case.
221 newIndex = dir == Dir.FORWARD ? this.getText().length - 1 : 0;
223 case Movement.DIRECTIONAL:
224 var pred = unit == Unit.NODE ?
225 AutomationPredicate.leaf : AutomationPredicate.leafDomNode;
226 newNode = AutomationUtil.findNextNode(
227 newNode, dir, pred) || this.node_;
228 newIndex = cursors.NODE_INDEX;
236 newNode = AutomationUtil.findNodeUntil(newNode, dir,
237 AutomationPredicate.linebreak, {before: true});
238 newNode = newNode || this.node_;
240 dir == Dir.FORWARD ? this.getText(newNode).length : 0;
242 case Movement.DIRECTIONAL:
243 newNode = AutomationUtil.findNodeUntil(
244 newNode, dir, AutomationPredicate.linebreak);
249 throw 'Unrecognized unit: ' + unit;
251 newNode = newNode || this.node_;
252 newIndex = goog.isDef(newIndex) ? newIndex : this.index_;
253 return new cursors.Cursor(newNode, newIndex);
258 * Represents a range in the automation tree. There is no visible selection on
259 * the page caused by usage of this object.
260 * It is assumed that the caller provides |start| and |end| in document order.
261 * @param {!cursors.Cursor} start
262 * @param {!cursors.Cursor} end
265 cursors.Range = function(start, end) {
266 /** @type {!cursors.Cursor} @private */
268 /** @type {!cursors.Cursor} @private */
273 * Convenience method to construct a Range surrounding one node.
274 * @param {!AutomationNode} node
275 * @return {!cursors.Range}
277 cursors.Range.fromNode = function(node) {
278 var cursor = cursors.Cursor.fromNode(node);
279 return new cursors.Range(cursor, cursor);
283 * Given |rangeA| and |rangeB| in order, determine which |Dir|
285 * @param {!cursors.Range} rangeA
286 * @param {!cursors.Range} rangeB
289 cursors.Range.getDirection = function(rangeA, rangeB) {
290 if (!rangeA || !rangeB)
293 // They are the same range.
294 if (rangeA.start.node === rangeB.start.node &&
295 rangeB.end.node === rangeA.end.node)
299 AutomationUtil.getDirection(
300 rangeA.start.node, rangeB.end.node);
302 AutomationUtil.getDirection(
303 rangeB.start.node, rangeA.end.node);
305 // The two ranges are either partly overlapping or non overlapping.
306 if (testDirA == Dir.FORWARD && testDirB == Dir.BACKWARD)
308 else if (testDirA == Dir.BACKWARD && testDirB == Dir.FORWARD)
314 cursors.Range.prototype = {
316 * Returns true if |rhs| is equal to this range.
317 * @param {!cursors.Range} rhs
320 equals: function(rhs) {
321 return this.start_.equals(rhs.start) &&
322 this.end_.equals(rhs.end);
326 * Gets a cursor bounding this range.
327 * @param {Dir} dir Which endpoint cursor to return; Dir.FORWARD for end,
328 * Dir.BACKWARD for start.
329 * @param {boolean=} opt_reverse Specify to have Dir.BACKWARD return end,
330 * Dir.FORWARD return start.
331 * @return {!cursors.Cursor}
333 getBound: function(dir, opt_reverse) {
335 return dir == Dir.BACKWARD ? this.end_ : this.start_;
336 return dir == Dir.FORWARD ? this.end_ : this.start_;
340 * @return {!cursors.Cursor}
347 * @return {!cursors.Cursor}
354 * Returns true if this range covers less than a node.
357 isSubNode: function() {
358 return this.start.node === this.end.node &&
359 this.start.getIndex() > -1 &&
360 this.end.getIndex() > -1;
364 * Makes a Range which has been moved from this range by the given unit and
368 * @return {cursors.Range}
370 move: function(unit, dir) {
371 var newStart = this.start_;
372 var newEnd = newStart;
375 newStart = newStart.move(unit, Movement.BOUND, dir);
376 newEnd = newStart.move(unit, Movement.BOUND, Dir.FORWARD);
377 // Character crossed a node; collapses to the end of the node.
378 if (newStart.node !== newEnd.node)
383 newStart = newStart.move(unit, Movement.DIRECTIONAL, dir);
384 newStart = newStart.move(unit, Movement.BOUND, Dir.BACKWARD);
385 newEnd = newStart.move(unit, Movement.BOUND, Dir.FORWARD);
389 newStart = newStart.move(unit, Movement.DIRECTIONAL, dir);
393 return new cursors.Range(newStart, newEnd);