OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview Classes related to cursors that point to and select parts of | 6 * @fileoverview Classes related to cursors that point to and select parts of |
7 * the automation tree. | 7 * the automation tree. |
8 */ | 8 */ |
9 | 9 |
10 goog.provide('cursors.Cursor'); | 10 goog.provide('cursors.Cursor'); |
11 goog.provide('cursors.Movement'); | 11 goog.provide('cursors.Movement'); |
12 goog.provide('cursors.Range'); | 12 goog.provide('cursors.Range'); |
13 goog.provide('cursors.Unit'); | 13 goog.provide('cursors.Unit'); |
14 | 14 |
| 15 goog.require('AutomationPredicate'); |
15 goog.require('AutomationUtil'); | 16 goog.require('AutomationUtil'); |
16 goog.require('StringUtil'); | 17 goog.require('StringUtil'); |
17 goog.require('constants'); | 18 goog.require('constants'); |
18 | 19 |
19 /** | 20 /** |
20 * The special index that represents a cursor pointing to a node without | 21 * The special index that represents a cursor pointing to a node without |
21 * pointing to any part of its accessible text. | 22 * pointing to any part of its accessible text. |
22 */ | 23 */ |
23 cursors.NODE_INDEX = -1; | 24 cursors.NODE_INDEX = -1; |
24 | 25 |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
64 | 65 |
65 /** | 66 /** |
66 * Represents a position within the automation tree. | 67 * Represents a position within the automation tree. |
67 * @constructor | 68 * @constructor |
68 * @param {!AutomationNode} node | 69 * @param {!AutomationNode} node |
69 * @param {number} index A 0-based index into this cursor node's primary | 70 * @param {number} index A 0-based index into this cursor node's primary |
70 * accessible name. An index of |cursors.NODE_INDEX| means the node as a whole | 71 * accessible name. An index of |cursors.NODE_INDEX| means the node as a whole |
71 * is pointed to and covers the case where the accessible text is empty. | 72 * is pointed to and covers the case where the accessible text is empty. |
72 */ | 73 */ |
73 cursors.Cursor = function(node, index) { | 74 cursors.Cursor = function(node, index) { |
74 /** @type {!AutomationNode} @private */ | |
75 this.node_ = node; | |
76 /** @type {number} @private */ | 75 /** @type {number} @private */ |
77 this.index_ = index; | 76 this.index_ = index; |
| 77 /** @type {Array<AutomationNode>} @private */ |
| 78 this.ancestry_ = []; |
| 79 var nodeWalker = node; |
| 80 while (nodeWalker) { |
| 81 this.ancestry_.push(nodeWalker); |
| 82 nodeWalker = nodeWalker.parent; |
| 83 if (nodeWalker && AutomationPredicate.root(nodeWalker)) |
| 84 break; |
| 85 } |
78 }; | 86 }; |
79 | 87 |
80 /** | 88 /** |
81 * Convenience method to construct a Cursor from a node. | 89 * Convenience method to construct a Cursor from a node. |
82 * @param {!AutomationNode} node | 90 * @param {!AutomationNode} node |
83 * @return {!cursors.Cursor} | 91 * @return {!cursors.Cursor} |
84 */ | 92 */ |
85 cursors.Cursor.fromNode = function(node) { | 93 cursors.Cursor.fromNode = function(node) { |
86 return new cursors.Cursor(node, cursors.NODE_INDEX); | 94 return new cursors.Cursor(node, cursors.NODE_INDEX); |
87 }; | 95 }; |
88 | 96 |
89 cursors.Cursor.prototype = { | 97 cursors.Cursor.prototype = { |
90 /** | 98 /** |
91 * Returns true if |rhs| is equal to this cursor. | 99 * Returns true if |rhs| is equal to this cursor. |
92 * @param {!cursors.Cursor} rhs | 100 * @param {!cursors.Cursor} rhs |
93 * @return {boolean} | 101 * @return {boolean} |
94 */ | 102 */ |
95 equals: function(rhs) { | 103 equals: function(rhs) { |
96 return this.node_ === rhs.node && | 104 return this.node === rhs.node && |
97 this.index_ === rhs.index; | 105 this.index === rhs.index; |
98 }, | 106 }, |
99 | 107 |
100 /** | 108 /** |
101 * @return {!AutomationNode} | 109 * Returns the node. If the node is invalid since the last time it |
| 110 * was accessed, moves the cursor to the nearest valid ancestor first. |
| 111 * @return {AutomationNode} |
102 */ | 112 */ |
103 get node() { | 113 get node() { |
104 return this.node_; | 114 for (var i = 0; i < this.ancestry_.length; i++) { |
| 115 var firstValidNode = this.ancestry_[i]; |
| 116 if (firstValidNode != null && firstValidNode.role !== undefined && |
| 117 firstValidNode.root !== undefined) { |
| 118 return firstValidNode; |
| 119 } |
| 120 // If we have to walk up to an ancestor, reset the index to NODE_INDEX. |
| 121 this.index_ = cursors.NODE_INDEX; |
| 122 } |
| 123 return null; |
105 }, | 124 }, |
106 | 125 |
107 /** | 126 /** |
108 * @return {number} | 127 * @return {number} |
109 */ | 128 */ |
110 get index() { | 129 get index() { |
111 return this.index_; | 130 return this.index_; |
112 }, | 131 }, |
113 | 132 |
114 /** | 133 /** |
(...skipping 19 matching lines...) Expand all Loading... |
134 }, | 153 }, |
135 | 154 |
136 /** | 155 /** |
137 * Gets the accessible text of the node associated with this cursor. | 156 * Gets the accessible text of the node associated with this cursor. |
138 * | 157 * |
139 * @param {!AutomationNode=} opt_node Use this node rather than this cursor's | 158 * @param {!AutomationNode=} opt_node Use this node rather than this cursor's |
140 * node. | 159 * node. |
141 * @return {string} | 160 * @return {string} |
142 */ | 161 */ |
143 getText: function(opt_node) { | 162 getText: function(opt_node) { |
144 var node = opt_node || this.node_; | 163 var node = opt_node || this.node; |
145 if (node.role === RoleType.textField) | 164 if (node.role === RoleType.textField) |
146 return node.value; | 165 return node.value; |
147 return node.name || ''; | 166 return node.name || ''; |
148 }, | 167 }, |
149 | 168 |
150 /** | 169 /** |
151 * Makes a Cursor which has been moved from this cursor by the unit in the | 170 * Makes a Cursor which has been moved from this cursor by the unit in the |
152 * given direction using the given movement type. | 171 * given direction using the given movement type. |
153 * @param {Unit} unit | 172 * @param {Unit} unit |
154 * @param {Movement} movement | 173 * @param {Movement} movement |
155 * @param {Dir} dir | 174 * @param {Dir} dir |
156 * @return {!cursors.Cursor} The moved cursor. | 175 * @return {!cursors.Cursor} The moved cursor. |
157 */ | 176 */ |
158 move: function(unit, movement, dir) { | 177 move: function(unit, movement, dir) { |
159 var newNode = this.node_; | 178 var originalNode = this.node; |
| 179 if (!originalNode) |
| 180 return this; |
| 181 |
| 182 var newNode = originalNode; |
160 var newIndex = this.index_; | 183 var newIndex = this.index_; |
161 | 184 |
162 if ((unit != Unit.NODE || unit != Unit.DOM_NODE) && | 185 if ((unit != Unit.NODE || unit != Unit.DOM_NODE) && |
163 newIndex === cursors.NODE_INDEX) | 186 newIndex === cursors.NODE_INDEX) |
164 newIndex = 0; | 187 newIndex = 0; |
165 | 188 |
166 switch (unit) { | 189 switch (unit) { |
167 case Unit.CHARACTER: | 190 case Unit.CHARACTER: |
168 // BOUND and DIRECTIONAL are the same for characters. | 191 // BOUND and DIRECTIONAL are the same for characters. |
169 var text = this.getText(); | 192 var text = this.getText(); |
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
250 case Unit.NODE: | 273 case Unit.NODE: |
251 case Unit.DOM_NODE: | 274 case Unit.DOM_NODE: |
252 switch (movement) { | 275 switch (movement) { |
253 case Movement.BOUND: | 276 case Movement.BOUND: |
254 newIndex = dir == Dir.FORWARD ? this.getText().length - 1 : 0; | 277 newIndex = dir == Dir.FORWARD ? this.getText().length - 1 : 0; |
255 break; | 278 break; |
256 case Movement.DIRECTIONAL: | 279 case Movement.DIRECTIONAL: |
257 var pred = unit == Unit.NODE ? | 280 var pred = unit == Unit.NODE ? |
258 AutomationPredicate.leaf : AutomationPredicate.object; | 281 AutomationPredicate.leaf : AutomationPredicate.object; |
259 newNode = AutomationUtil.findNextNode( | 282 newNode = AutomationUtil.findNextNode( |
260 newNode, dir, pred) || this.node_; | 283 newNode, dir, pred) || originalNode; |
261 newIndex = cursors.NODE_INDEX; | 284 newIndex = cursors.NODE_INDEX; |
262 break; | 285 break; |
263 } | 286 } |
264 break; | 287 break; |
265 case Unit.LINE: | 288 case Unit.LINE: |
266 newIndex = 0; | 289 newIndex = 0; |
267 switch (movement) { | 290 switch (movement) { |
268 case Movement.BOUND: | 291 case Movement.BOUND: |
269 newNode = AutomationUtil.findNodeUntil(newNode, dir, | 292 newNode = AutomationUtil.findNodeUntil(newNode, dir, |
270 AutomationPredicate.linebreak, true); | 293 AutomationPredicate.linebreak, true); |
271 newNode = newNode || this.node_; | 294 newNode = newNode || originalNode; |
272 newIndex = | 295 newIndex = |
273 dir == Dir.FORWARD ? this.getText(newNode).length : 0; | 296 dir == Dir.FORWARD ? this.getText(newNode).length : 0; |
274 break; | 297 break; |
275 case Movement.DIRECTIONAL: | 298 case Movement.DIRECTIONAL: |
276 newNode = AutomationUtil.findNodeUntil( | 299 newNode = AutomationUtil.findNodeUntil( |
277 newNode, dir, AutomationPredicate.linebreak); | 300 newNode, dir, AutomationPredicate.linebreak); |
278 break; | 301 break; |
279 } | 302 } |
280 break; | 303 break; |
281 default: | 304 default: |
282 throw Error('Unrecognized unit: ' + unit); | 305 throw Error('Unrecognized unit: ' + unit); |
283 } | 306 } |
284 newNode = newNode || this.node_; | 307 newNode = newNode || originalNode; |
285 newIndex = goog.isDef(newIndex) ? newIndex : this.index_; | 308 newIndex = goog.isDef(newIndex) ? newIndex : this.index_; |
286 return new cursors.Cursor(newNode, newIndex); | 309 return new cursors.Cursor(newNode, newIndex); |
287 }, | 310 }, |
288 | 311 |
289 /** | 312 /** |
290 * Returns whether this cursor points to a valid position. | 313 * Returns whether this cursor points to a valid position. |
291 * @return {boolean} | 314 * @return {boolean} |
292 */ | 315 */ |
293 isValid: function() { | 316 isValid: function() { |
294 return !!this.node.root; | 317 return !!this.node && !!this.node.root; |
295 } | 318 } |
296 }; | 319 }; |
297 | 320 |
298 /** | 321 /** |
299 * A cursors.Cursor that wraps from beginning to end and vice versa when moved. | 322 * A cursors.Cursor that wraps from beginning to end and vice versa when moved. |
300 * @constructor | 323 * @constructor |
301 * @param {!AutomationNode} node | 324 * @param {!AutomationNode} node |
302 * @param {number} index A 0-based index into this cursor node's primary | 325 * @param {number} index A 0-based index into this cursor node's primary |
303 * accessible name. An index of |cursors.NODE_INDEX| means the node as a whole | 326 * accessible name. An index of |cursors.NODE_INDEX| means the node as a whole |
304 * is pointed to and covers the case where the accessible text is empty. | 327 * is pointed to and covers the case where the accessible text is empty. |
(...skipping 12 matching lines...) Expand all Loading... |
317 cursors.WrappingCursor.fromNode = function(node) { | 340 cursors.WrappingCursor.fromNode = function(node) { |
318 return new cursors.WrappingCursor(node, cursors.NODE_INDEX); | 341 return new cursors.WrappingCursor(node, cursors.NODE_INDEX); |
319 }; | 342 }; |
320 | 343 |
321 cursors.WrappingCursor.prototype = { | 344 cursors.WrappingCursor.prototype = { |
322 __proto__: cursors.Cursor.prototype, | 345 __proto__: cursors.Cursor.prototype, |
323 | 346 |
324 /** @override */ | 347 /** @override */ |
325 move: function(unit, movement, dir) { | 348 move: function(unit, movement, dir) { |
326 var result = this; | 349 var result = this; |
| 350 if (!result.node) |
| 351 return this; |
327 | 352 |
328 // Regular movement. | 353 // Regular movement. |
329 if (!AutomationPredicate.root(this.node) || dir == Dir.FORWARD) | 354 if (!AutomationPredicate.root(this.node) || dir == Dir.FORWARD) |
330 result = cursors.Cursor.prototype.move.call(this, unit, movement, dir); | 355 result = cursors.Cursor.prototype.move.call(this, unit, movement, dir); |
331 | 356 |
332 // There are two cases for wrapping: | 357 // There are two cases for wrapping: |
333 // 1. moving forwards from the last element. | 358 // 1. moving forwards from the last element. |
334 // 2. moving backwards from the first element. | 359 // 2. moving backwards from the first element. |
335 // Both result in |move| returning the same cursor. | 360 // Both result in |move| returning the same cursor. |
336 // For 1, simply place the new cursor on the document node. | 361 // For 1, simply place the new cursor on the document node. |
337 // For 2, place range on the root (if not already there). If at root, | 362 // For 2, place range on the root (if not already there). If at root, |
338 // try to descend to the first leaf-like object. | 363 // try to descend to the first leaf-like object. |
339 if (movement == Movement.DIRECTIONAL && result.equals(this)) { | 364 if (movement == Movement.DIRECTIONAL && result.equals(this)) { |
340 var pred = unit == Unit.DOM_NODE ? | 365 var pred = unit == Unit.DOM_NODE ? |
341 AutomationPredicate.object : AutomationPredicate.leaf; | 366 AutomationPredicate.object : AutomationPredicate.leaf; |
342 var endpoint = this.node; | 367 var endpoint = this.node; |
| 368 if (!endpoint) |
| 369 return this; |
343 | 370 |
344 // Case 1: forwards (find the root-like node). | 371 // Case 1: forwards (find the root-like node). |
345 while (!AutomationPredicate.root(endpoint) && endpoint.parent) | 372 while (!AutomationPredicate.root(endpoint) && endpoint.parent) |
346 endpoint = endpoint.parent; | 373 endpoint = endpoint.parent; |
347 | 374 |
348 // Always play a wrap earcon when moving forward. | 375 // Always play a wrap earcon when moving forward. |
349 var playEarcon = dir == Dir.FORWARD; | 376 var playEarcon = dir == Dir.FORWARD; |
350 | 377 |
351 // Case 2: backward (sync downwards to a leaf), if already on the root. | 378 // Case 2: backward (sync downwards to a leaf), if already on the root. |
352 if (dir == Dir.BACKWARD && endpoint == this.node_) { | 379 if (dir == Dir.BACKWARD && endpoint == this.node) { |
353 playEarcon = true; | 380 playEarcon = true; |
354 endpoint = AutomationUtil.findNodePre(endpoint, | 381 endpoint = AutomationUtil.findNodePre(endpoint, |
355 dir, | 382 dir, |
356 function(n) { | 383 function(n) { |
357 return pred(n) && !AutomationPredicate.shouldIgnoreNode(n); | 384 return pred(n) && !AutomationPredicate.shouldIgnoreNode(n); |
358 }) || endpoint; | 385 }) || endpoint; |
359 } | 386 } |
360 | 387 |
361 if (playEarcon) | 388 if (playEarcon) |
362 cvox.ChromeVox.earcons.playEarcon(cvox.Earcon.WRAP); | 389 cvox.ChromeVox.earcons.playEarcon(cvox.Earcon.WRAP); |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
396 * Given |rangeA| and |rangeB| in order, determine which |Dir| | 423 * Given |rangeA| and |rangeB| in order, determine which |Dir| |
397 * relates them. | 424 * relates them. |
398 * @param {!cursors.Range} rangeA | 425 * @param {!cursors.Range} rangeA |
399 * @param {!cursors.Range} rangeB | 426 * @param {!cursors.Range} rangeB |
400 * @return {Dir} | 427 * @return {Dir} |
401 */ | 428 */ |
402 cursors.Range.getDirection = function(rangeA, rangeB) { | 429 cursors.Range.getDirection = function(rangeA, rangeB) { |
403 if (!rangeA || !rangeB) | 430 if (!rangeA || !rangeB) |
404 return Dir.FORWARD; | 431 return Dir.FORWARD; |
405 | 432 |
| 433 if (!rangeA.start.node || !rangeA.end.node || |
| 434 !rangeB.start.node || !rangeB.end.node) |
| 435 return Dir.FORWARD; |
| 436 |
406 // They are the same range. | 437 // They are the same range. |
407 if (rangeA.start.node === rangeB.start.node && | 438 if (rangeA.start.node === rangeB.start.node && |
408 rangeB.end.node === rangeA.end.node) | 439 rangeB.end.node === rangeA.end.node) |
409 return Dir.FORWARD; | 440 return Dir.FORWARD; |
410 | 441 |
411 var testDirA = | 442 var testDirA = |
412 AutomationUtil.getDirection( | 443 AutomationUtil.getDirection( |
413 rangeA.start.node, rangeB.end.node); | 444 rangeA.start.node, rangeB.end.node); |
414 var testDirB = | 445 var testDirB = |
415 AutomationUtil.getDirection( | 446 AutomationUtil.getDirection( |
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
475 | 506 |
476 /** | 507 /** |
477 * Makes a Range which has been moved from this range by the given unit and | 508 * Makes a Range which has been moved from this range by the given unit and |
478 * direction. | 509 * direction. |
479 * @param {Unit} unit | 510 * @param {Unit} unit |
480 * @param {Dir} dir | 511 * @param {Dir} dir |
481 * @return {cursors.Range} | 512 * @return {cursors.Range} |
482 */ | 513 */ |
483 move: function(unit, dir) { | 514 move: function(unit, dir) { |
484 var newStart = this.start_; | 515 var newStart = this.start_; |
| 516 if (!newStart.node) |
| 517 return this; |
| 518 |
485 var newEnd; | 519 var newEnd; |
486 switch (unit) { | 520 switch (unit) { |
487 case Unit.CHARACTER: | 521 case Unit.CHARACTER: |
488 newStart = newStart.move(unit, Movement.DIRECTIONAL, dir); | 522 newStart = newStart.move(unit, Movement.DIRECTIONAL, dir); |
489 newEnd = newStart.move(unit, Movement.DIRECTIONAL, Dir.FORWARD); | 523 newEnd = newStart.move(unit, Movement.DIRECTIONAL, Dir.FORWARD); |
490 // Character crossed a node; collapses to the end of the node. | 524 // Character crossed a node; collapses to the end of the node. |
491 if (newStart.node !== newEnd.node) | 525 if (newStart.node !== newEnd.node) |
492 newEnd = new cursors.Cursor(newStart.node, newStart.index + 1); | 526 newEnd = new cursors.Cursor(newStart.node, newStart.index + 1); |
493 break; | 527 break; |
494 case Unit.WORD: | 528 case Unit.WORD: |
(...skipping 13 matching lines...) Expand all Loading... |
508 return new cursors.Range(newStart, newEnd); | 542 return new cursors.Range(newStart, newEnd); |
509 }, | 543 }, |
510 | 544 |
511 /** | 545 /** |
512 * Select the text contained within this range. | 546 * Select the text contained within this range. |
513 */ | 547 */ |
514 select: function() { | 548 select: function() { |
515 var start = this.start.node; | 549 var start = this.start.node; |
516 var end = this.end.node; | 550 var end = this.end.node; |
517 | 551 |
| 552 if (!start || !end) |
| 553 return; |
| 554 |
518 // Find the most common root. | 555 // Find the most common root. |
519 var uniqueAncestors = AutomationUtil.getUniqueAncestors(start, end); | 556 var uniqueAncestors = AutomationUtil.getUniqueAncestors(start, end); |
520 var mcr = start.root; | 557 var mcr = start.root; |
521 if (uniqueAncestors) | 558 if (uniqueAncestors) |
522 mcr = uniqueAncestors.pop().parent.root; | 559 mcr = uniqueAncestors.pop().parent.root; |
523 | 560 |
524 if (mcr.role == RoleType.desktop) | 561 if (mcr.role == RoleType.desktop) |
525 return; | 562 return; |
526 | 563 |
527 if (mcr === start.root && mcr === end.root) { | 564 if (mcr === start.root && mcr === end.root) { |
(...skipping 25 matching lines...) Expand all Loading... |
553 /** | 590 /** |
554 * Returns whether this range has valid start and end cursors. | 591 * Returns whether this range has valid start and end cursors. |
555 * @return {boolean} | 592 * @return {boolean} |
556 */ | 593 */ |
557 isValid: function() { | 594 isValid: function() { |
558 return this.start.isValid() && this.end.isValid(); | 595 return this.start.isValid() && this.end.isValid(); |
559 } | 596 } |
560 }; | 597 }; |
561 | 598 |
562 }); // goog.scope | 599 }); // goog.scope |
OLD | NEW |