Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(59)

Side by Side Diff: chrome/browser/resources/chromeos/chromevox/cvox2/background/cursors.js

Issue 2007183002: Make ChromeVox cursor robust to deleted nodes (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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
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
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
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
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698