Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / cvox2 / background / cursors.js
blobd92d55adccea55439ef835ad676c9e733c3f759b
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.
5 /**
6  * @fileoverview Classes related to cursors that point to and select parts of
7  * the automation tree.
8  */
10 goog.provide('cursors.Cursor');
11 goog.provide('cursors.Movement');
12 goog.provide('cursors.Range');
13 goog.provide('cursors.Unit');
15 goog.require('AutomationUtil');
17 /**
18  * The special index that represents a cursor pointing to a node without
19  * pointing to any part of its accessible text.
20  */
21 cursors.NODE_INDEX = -1;
23 /**
24  * Represents units of CursorMovement.
25  * @enum {string}
26  */
27 cursors.Unit = {
28   /** A single character within accessible name or value. */
29   CHARACTER: 'character',
31   /** A range of characters (given by attributes on automation nodes). */
32   WORD: 'word',
34   /** A leaf node. */
35   NODE: 'node',
37   /** A leaf DOM-node. */
38   DOM_NODE: 'dom_node',
40   /** Formed by a set of leaf nodes that are inline. */
41   LINE: 'line'
44 /**
45  * Represents the ways in which cursors can move given a cursor unit.
46  * @enum {string}
47  */
48 cursors.Movement = {
49   /** Move to the beginning or end of the current unit. */
50   BOUND: 'bound',
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;
63 /**
64  * Represents a position within the automation tree.
65  * @constructor
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.
71  */
72 cursors.Cursor = function(node, index) {
73   /** @type {!AutomationNode} @private */
74   this.node_ = node;
75   /** @type {number} @private */
76   this.index_ = index;
79 /**
80  * Convenience method to construct a Cursor from a node.
81  * @param {!AutomationNode} node
82  * @return {!cursors.Cursor}
83  */
84 cursors.Cursor.fromNode = function(node) {
85   return new cursors.Cursor(node, cursors.NODE_INDEX);
88 cursors.Cursor.prototype = {
89   /**
90    * Returns true if |rhs| is equal to this cursor.
91    * @param {!cursors.Cursor} rhs
92    * @return {boolean}
93    */
94   equals: function(rhs) {
95     return this.node_ === rhs.node &&
96         this.index_ === rhs.getIndex();
97   },
99   /**
100    * @return {!AutomationNode}
101    */
102   get node() {
103     return this.node_;
104   },
106   /**
107    * @return {number}
108    */
109   getIndex: function() {
110     return this.index_;
111   },
113   /**
114    * Gets the accessible text of the node associated with this cursor.
115    *
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
120    * node.
121    * @return {string}
122    */
123   getText: function(opt_node) {
124     var node = opt_node || this.node_;
125     return node.name || node.value || '';
126   },
128   /**
129    * Makes a Cursor which has been moved from this cursor by the unit in the
130    * given direction using the given movement type.
131    * @param {Unit} unit
132    * @param {Movement} movement
133    * @param {Dir} dir
134    * @return {!cursors.Cursor} The moved cursor.
135    */
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)
142       newIndex = 0;
144     switch (unit) {
145       case Unit.CHARACTER:
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);
151           if (newNode) {
152             newIndex =
153                 dir == Dir.FORWARD ? 0 : this.getText(newNode).length - 1;
154             newIndex = newIndex == -1 ? 0 : newIndex;
155           } else {
156             newIndex = this.index_;
157           }
158         }
159         break;
160       case Unit.WORD:
161         switch (movement) {
162           case Movement.BOUND:
163             if (newNode.role == Role.inlineTextBox) {
164               var start, end;
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];
170                   break;
171                 }
172               }
173               if (goog.isDef(start) && goog.isDef(end))
174                 newIndex = dir == Dir.FORWARD ? end : start;
175             } else {
176               // TODO(dtseng): Figure out what to do in this case.
177             }
178             break;
179           case Movement.DIRECTIONAL:
180             if (newNode.role == Role.inlineTextBox) {
181               var start, end;
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];
188                   break;
189                 }
190               }
191               if (goog.isDef(start)) {
192                 newIndex = start;
193               } else {
194                 // The backward case is special at the beginning of nodes.
195                 if (dir == Dir.BACKWARD && newIndex != 0) {
196                   newIndex = 0;
197                 } else {
198                   newNode = AutomationUtil.findNextNode(newNode, dir,
199                       AutomationPredicate.leaf);
200                   if (newNode) {
201                     newIndex = 0;
202                     if (dir == Dir.BACKWARD &&
203                         newNode.role == Role.inlineTextBox) {
204                       var starts = newNode.wordStarts;
205                       newIndex = starts[starts.length - 1] || 0;
206                     } else {
207                       // TODO(dtseng): Figure out what to do for general nodes.
208                     }
209                   }
210                 }
211               }
212             } else {
213               // TODO(dtseng): Figure out what to do in this case.
214             }
215         }
216         break;
217       case Unit.NODE:
218       case Unit.DOM_NODE:
219         switch (movement) {
220           case Movement.BOUND:
221             newIndex = dir == Dir.FORWARD ? this.getText().length - 1 : 0;
222             break;
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;
229             break;
230         }
231         break;
232       case Unit.LINE:
233         newIndex = 0;
234         switch (movement) {
235           case Movement.BOUND:
236             newNode = AutomationUtil.findNodeUntil(newNode, dir,
237                 AutomationPredicate.linebreak, {before: true});
238             newNode = newNode || this.node_;
239             newIndex =
240                 dir == Dir.FORWARD ? this.getText(newNode).length : 0;
241             break;
242           case Movement.DIRECTIONAL:
243             newNode = AutomationUtil.findNodeUntil(
244                 newNode, dir, AutomationPredicate.linebreak);
245             break;
246           }
247       break;
248       default:
249         throw 'Unrecognized unit: ' + unit;
250     }
251     newNode = newNode || this.node_;
252     newIndex = goog.isDef(newIndex) ? newIndex : this.index_;
253     return new cursors.Cursor(newNode, newIndex);
254   }
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
263  * @constructor
264  */
265 cursors.Range = function(start, end) {
266   /** @type {!cursors.Cursor} @private */
267   this.start_ = start;
268   /** @type {!cursors.Cursor} @private */
269   this.end_ = end;
273  * Convenience method to construct a Range surrounding one node.
274  * @param {!AutomationNode} node
275  * @return {!cursors.Range}
276  */
277 cursors.Range.fromNode = function(node) {
278   var cursor = cursors.Cursor.fromNode(node);
279   return new cursors.Range(cursor, cursor);
282  /**
283  * Given |rangeA| and |rangeB| in order, determine which |Dir|
284  * relates them.
285  * @param {!cursors.Range} rangeA
286  * @param {!cursors.Range} rangeB
287  * @return {Dir}
288  */
289 cursors.Range.getDirection = function(rangeA, rangeB) {
290   if (!rangeA || !rangeB)
291     return Dir.FORWARD;
293   // They are the same range.
294   if (rangeA.start.node === rangeB.start.node &&
295       rangeB.end.node === rangeA.end.node)
296     return Dir.FORWARD;
298   var testDirA =
299       AutomationUtil.getDirection(
300           rangeA.start.node, rangeB.end.node);
301   var testDirB =
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)
307     return Dir.FORWARD;
308   else if (testDirA == Dir.BACKWARD && testDirB == Dir.FORWARD)
309     return Dir.BACKWARD;
310   else
311     return testDirA;
314 cursors.Range.prototype = {
315   /**
316    * Returns true if |rhs| is equal to this range.
317    * @param {!cursors.Range} rhs
318    * @return {boolean}
319    */
320   equals: function(rhs) {
321     return this.start_.equals(rhs.start) &&
322         this.end_.equals(rhs.end);
323   },
325   /**
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}
332    */
333   getBound: function(dir, opt_reverse) {
334     if (opt_reverse)
335       return dir == Dir.BACKWARD ? this.end_ : this.start_;
336     return dir == Dir.FORWARD ? this.end_ : this.start_;
337   },
339   /**
340    * @return {!cursors.Cursor}
341    */
342   get start() {
343     return this.start_;
344   },
346   /**
347    * @return {!cursors.Cursor}
348    */
349   get end() {
350     return this.end_;
351   },
353   /**
354    * Returns true if this range covers less than a node.
355    * @return {boolean}
356    */
357   isSubNode: function() {
358     return this.start.node === this.end.node &&
359         this.start.getIndex() > -1 &&
360         this.end.getIndex() > -1;
361   },
363   /**
364    * Makes a Range which has been moved from this range by the given unit and
365    * direction.
366    * @param {Unit} unit
367    * @param {Dir} dir
368    * @return {cursors.Range}
369    */
370   move: function(unit, dir) {
371     var newStart = this.start_;
372     var newEnd = newStart;
373     switch (unit) {
374       case Unit.CHARACTER:
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)
379           newEnd = newStart;
380         break;
381       case Unit.WORD:
382       case Unit.LINE:
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);
386         break;
387       case Unit.NODE:
388       case Unit.DOM_NODE:
389         newStart = newStart.move(unit, Movement.DIRECTIONAL, dir);
390         newEnd = newStart;
391         break;
392     }
393     return new cursors.Range(newStart, newEnd);
394   }
397 });  // goog.scope