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. 4 5/** 6 * @fileoverview Manages navigation within a page. 7 * This unifies navigation by the DOM walker and by WebKit selection. 8 * NOTE: the purpose of this class is only to hold state 9 * and delegate all of its functionality to mostly stateless classes that 10 * are easy to test. 11 * 12 */ 13 14 15goog.provide('cvox.NavigationManager'); 16 17goog.require('cvox.ActiveIndicator'); 18goog.require('cvox.ChromeVox'); 19goog.require('cvox.ChromeVoxEventSuspender'); 20goog.require('cvox.CursorSelection'); 21goog.require('cvox.DescriptionUtil'); 22goog.require('cvox.DomUtil'); 23goog.require('cvox.FindUtil'); 24goog.require('cvox.Focuser'); 25goog.require('cvox.Interframe'); 26goog.require('cvox.MathShifter'); 27goog.require('cvox.NavBraille'); 28goog.require('cvox.NavDescription'); 29goog.require('cvox.NavigationHistory'); 30goog.require('cvox.NavigationShifter'); 31goog.require('cvox.NavigationSpeaker'); 32goog.require('cvox.PageSelection'); 33goog.require('cvox.SelectionUtil'); 34goog.require('cvox.TableShifter'); 35goog.require('cvox.TraverseMath'); 36goog.require('cvox.Widget'); 37 38 39/** 40 * @constructor 41 */ 42cvox.NavigationManager = function() { 43 this.addInterframeListener_(); 44 45 this.reset(); 46}; 47 48/** 49 * Stores state variables in a provided object. 50 * 51 * @param {Object} store The object. 52 */ 53cvox.NavigationManager.prototype.storeOn = function(store) { 54 store['reversed'] = this.isReversed(); 55 store['keepReading'] = this.keepReading_; 56 store['findNext'] = this.predicate_; 57 this.shifter_.storeOn(store); 58}; 59 60/** 61 * Updates the object with state variables from an earlier storeOn call. 62 * 63 * @param {Object} store The object. 64 */ 65cvox.NavigationManager.prototype.readFrom = function(store) { 66 this.curSel_.setReversed(store['reversed']); 67 this.shifter_.readFrom(store); 68 if (store['keepReading']) { 69 this.startReading(cvox.AbstractTts.QUEUE_MODE_FLUSH); 70 } 71}; 72 73/** 74 * Resets the navigation manager to the top of the page. 75 */ 76cvox.NavigationManager.prototype.reset = function() { 77 /** 78 * @type {!cvox.NavigationSpeaker} 79 * @private 80 */ 81 this.navSpeaker_ = new cvox.NavigationSpeaker(); 82 83 /** 84 * @type {!Array.<Object>} 85 * @private 86 */ 87 this.shifterTypes_ = [cvox.NavigationShifter, 88 cvox.TableShifter, 89 cvox.MathShifter]; 90 91 /** 92 * @type {!Array.<!cvox.AbstractShifter>} 93 */ 94 this.shifterStack_ = []; 95 96 /** 97 * The active shifter. 98 * @type {!cvox.AbstractShifter} 99 * @private 100 */ 101 this.shifter_ = new cvox.NavigationShifter(); 102 103 // NOTE(deboer): document.activeElement can not be null (c.f. 104 // https://developer.mozilla.org/en-US/docs/DOM/document.activeElement) 105 // Instead, if there is no active element, activeElement is set to 106 // document.body. 107 /** 108 * If there is an activeElement, use it. Otherwise, sync to the page 109 * beginning. 110 * @type {!cvox.CursorSelection} 111 * @private 112 */ 113 this.curSel_ = document.activeElement != document.body ? 114 /** @type {!cvox.CursorSelection} **/ 115 (cvox.CursorSelection.fromNode(document.activeElement)) : 116 this.shifter_.begin(this.curSel_, {reversed: false}); 117 118 /** 119 * @type {!cvox.CursorSelection} 120 * @private 121 */ 122 this.prevSel_ = this.curSel_.clone(); 123 124 /** 125 * Keeps track of whether we have skipped while "reading from here" 126 * so that we can insert an earcon. 127 * @type {boolean} 128 * @private 129 */ 130 this.skipped_ = false; 131 132 /** 133 * Keeps track of whether we have recovered from dropped focus 134 * so that we can insert an earcon. 135 * @type {boolean} 136 * @private 137 */ 138 this.recovered_ = false; 139 140 /** 141 * True if in "reading from here" mode. 142 * @type {boolean} 143 * @private 144 */ 145 this.keepReading_ = false; 146 147 /** 148 * True if we are at the end of the page and we wrap around. 149 * @type {boolean} 150 * @private 151 */ 152 this.pageEnd_ = false; 153 154 /** 155 * True if we have already announced that we will wrap around. 156 * @type {boolean} 157 * @private 158 */ 159 this.pageEndAnnounced_ = false; 160 161 /** 162 * True if we entered into a shifter. 163 * @type {boolean} 164 * @private 165 */ 166 this.enteredShifter_ = false; 167 168 /** 169 * True if we exited a shifter. 170 * @type {boolean} 171 * @private 172 */ 173 this.exitedShifter_ = false; 174 175 /** 176 * True if we want to ignore iframes no matter what. 177 * @type {boolean} 178 * @private 179 */ 180 this.ignoreIframesNoMatterWhat_ = false; 181 182 /** 183 * @type {cvox.PageSelection} 184 * @private 185 */ 186 this.pageSel_ = null; 187 188 /** @type {string} */ 189 this.predicate_ = ''; 190 191 /** @type {cvox.CursorSelection} */ 192 this.saveSel_ = null; 193 194 // TODO(stoarca): This seems goofy. Why are we doing this? 195 if (this.activeIndicator) { 196 this.activeIndicator.removeFromDom(); 197 } 198 this.activeIndicator = new cvox.ActiveIndicator(); 199 200 /** 201 * Makes sure focus doesn't get lost. 202 * @type {!cvox.NavigationHistory} 203 * @private 204 */ 205 this.navigationHistory_ = new cvox.NavigationHistory(); 206 207 /** @type {boolean} */ 208 this.focusRecovery_ = window.location.protocol != 'chrome:'; 209 210 this.iframeIdMap = {}; 211 this.nextIframeId = 1; 212 213 // Only sync if the activeElement is not document.body; which is shorthand for 214 // 'no selection'. Currently the walkers don't deal with the no selection 215 // case -- and it is not clear that they should. 216 if (document.activeElement != document.body) { 217 this.sync(); 218 } 219 220 // This object is effectively empty when no math is in the page. 221 cvox.TraverseMath.getInstance(); 222}; 223 224 225/** 226 * Determines if we are navigating from a valid node. If not, ask navigation 227 * history for an acceptable restart point and go there. 228 * @param {function(Node)=} opt_predicate A function that takes in a node and 229 * returns true if it is a valid recovery candidate. 230 * @return {boolean} True if we should continue navigation normally. 231 */ 232cvox.NavigationManager.prototype.resolve = function(opt_predicate) { 233 if (!this.getFocusRecovery()) { 234 return true; 235 } 236 237 var current = this.getCurrentNode(); 238 239 if (!this.navigationHistory_.becomeInvalid(current)) { 240 return true; 241 } 242 243 // Only attempt to revert if going next will cause us to restart at the top 244 // of the page. 245 if (this.hasNext_()) { 246 return true; 247 } 248 249 // Our current node was invalid. Revert to history. 250 var revert = this.navigationHistory_.revert(opt_predicate); 251 252 // If the history is empty, revert.current will be null. In that case, 253 // it is best to continue navigating normally. 254 if (!revert.current) { 255 return true; 256 } 257 258 // Convert to selections. 259 var newSel = cvox.CursorSelection.fromNode(revert.current); 260 var context = cvox.CursorSelection.fromNode(revert.previous); 261 262 // Default to document body if selections are null. 263 newSel = newSel || cvox.CursorSelection.fromBody(); 264 context = context || cvox.CursorSelection.fromBody(); 265 newSel.setReversed(this.isReversed()); 266 267 this.updateSel(newSel, context); 268 this.recovered_ = true; 269 return false; 270}; 271 272 273/** 274 * Gets the state of focus recovery. 275 * @return {boolean} True if focus recovery is on; false otherwise. 276 */ 277cvox.NavigationManager.prototype.getFocusRecovery = function() { 278 return this.focusRecovery_; 279}; 280 281 282/** 283 * Enables or disables focus recovery. 284 * @param {boolean} value True to enable, false to disable. 285 */ 286cvox.NavigationManager.prototype.setFocusRecovery = function(value) { 287 this.focusRecovery_ = value; 288}; 289 290 291/** 292 * Delegates to NavigationShifter with current page state. 293 * @param {boolean=} iframes Jump in and out of iframes if true. Default false. 294 * @return {boolean} False if end of document has been reached. 295 * @private 296 */ 297cvox.NavigationManager.prototype.next_ = function(iframes) { 298 if (this.tryBoundaries_(this.shifter_.next(this.curSel_), iframes)) { 299 // TODO(dtseng): An observer interface would help to keep logic like this 300 // to a minimum. 301 this.pageSel_ && this.pageSel_.extend(this.curSel_); 302 return true; 303 } 304 return false; 305}; 306 307/** 308 * Looks ahead to see if it is possible to navigate forward from the current 309 * position. 310 * @return {boolean} True if it is possible to navigate forward. 311 * @private 312 */ 313cvox.NavigationManager.prototype.hasNext_ = function() { 314 // Non-default shifters validly end before page end. 315 if (this.shifterStack_.length > 0) { 316 return true; 317 } 318 var dummySel = this.curSel_.clone(); 319 var result = false; 320 var dummyNavShifter = new cvox.NavigationShifter(); 321 dummyNavShifter.setGranularity(this.shifter_.getGranularity()); 322 dummyNavShifter.sync(dummySel); 323 if (dummyNavShifter.next(dummySel)) { 324 result = true; 325 } 326 return result; 327}; 328 329 330/** 331 * Delegates to NavigationShifter with current page state. 332 * @param {function(Array.<Node>)} predicate A function taking an array 333 * of unique ancestor nodes as a parameter and returning a desired node. 334 * It returns null if that node can't be found. 335 * @param {string=} opt_predicateName The programmatic name that exists in 336 * cvox.DomPredicates. Used to dispatch calls across iframes since functions 337 * cannot be stringified. 338 * @param {boolean=} opt_initialNode Whether to start the search from node 339 * (true), or the next node (false); defaults to false. 340 * @return {cvox.CursorSelection} The newly found selection. 341 */ 342cvox.NavigationManager.prototype.findNext = function( 343 predicate, opt_predicateName, opt_initialNode) { 344 this.predicate_ = opt_predicateName || ''; 345 this.resolve(); 346 this.shifter_ = this.shifterStack_[0] || this.shifter_; 347 this.shifterStack_ = []; 348 var ret = cvox.FindUtil.findNext(this.curSel_, predicate, opt_initialNode); 349 if (!this.ignoreIframesNoMatterWhat_) { 350 this.tryIframe_(ret && ret.start.node); 351 } 352 if (ret) { 353 this.updateSelToArbitraryNode(ret.start.node); 354 } 355 this.predicate_ = ''; 356 return ret; 357}; 358 359 360/** 361 * Delegates to NavigationShifter with current page state. 362 */ 363cvox.NavigationManager.prototype.sync = function() { 364 this.resolve(); 365 var ret = this.shifter_.sync(this.curSel_); 366 if (ret) { 367 this.curSel_ = ret; 368 } 369}; 370 371/** 372 * Sync's all possible cursors: 373 * - focus 374 * - ActiveIndicator 375 * - CursorSelection 376 * @param {boolean=} opt_skipText Skips focus on text nodes; defaults to false. 377 */ 378cvox.NavigationManager.prototype.syncAll = function(opt_skipText) { 379 this.sync(); 380 this.setFocus(opt_skipText); 381 this.updateIndicator(); 382}; 383 384 385/** 386 * Clears a DOM selection made via a CursorSelection. 387 * @param {boolean=} opt_announce True to announce the clearing. 388 * @return {boolean} If a selection was cleared. 389 */ 390cvox.NavigationManager.prototype.clearPageSel = function(opt_announce) { 391 var hasSel = !!this.pageSel_; 392 if (hasSel && opt_announce) { 393 var announcement = cvox.ChromeVox.msgs.getMsg('clear_page_selection'); 394 cvox.ChromeVox.tts.speak(announcement, cvox.AbstractTts.QUEUE_MODE_FLUSH, 395 cvox.AbstractTts.PERSONALITY_ANNOTATION); 396 } 397 this.pageSel_ = null; 398 return hasSel; 399}; 400 401 402/** 403 * Begins or finishes a DOM selection at the current CursorSelection in the 404 * document. 405 * @return {boolean} Whether selection is on or off after this call. 406 */ 407cvox.NavigationManager.prototype.togglePageSel = function() { 408 this.pageSel_ = this.pageSel_ ? null : 409 new cvox.PageSelection(this.curSel_.setReversed(false)); 410 return !!this.pageSel_; 411}; 412 413 414// TODO(stoarca): getDiscription is split awkwardly between here and the 415// walkers. The walkers should have getBaseDescription() which requires 416// very little context, and then this method should tack on everything 417// which requires any extensive knowledge. 418/** 419 * Delegates to NavigationShifter with the current page state. 420 * @return {Array.<cvox.NavDescription>} The summary of the current position. 421 */ 422cvox.NavigationManager.prototype.getDescription = function() { 423 // Handle description of special content. Consider moving to DescriptionUtil. 424 // Specially annotated nodes. 425 if (this.getCurrentNode().hasAttribute && 426 this.getCurrentNode().hasAttribute('cvoxnodedesc')) { 427 var preDesc = cvox.ChromeVoxJSON.parse( 428 this.getCurrentNode().getAttribute('cvoxnodedesc')); 429 var currentDesc = new Array(); 430 for (var i = 0; i < preDesc.length; ++i) { 431 var inDesc = preDesc[i]; 432 // TODO: this can probably be replaced with just NavDescription(inDesc) 433 // need test case to ensure this change will work 434 currentDesc.push(new cvox.NavDescription({ 435 context: inDesc.context, 436 text: inDesc.text, 437 userValue: inDesc.userValue, 438 annotation: inDesc.annotation 439 })); 440 } 441 return currentDesc; 442 } 443 444 // Selected content. 445 var desc = this.pageSel_ ? this.pageSel_.getDescription( 446 this.shifter_, this.prevSel_, this.curSel_) : 447 this.shifter_.getDescription(this.prevSel_, this.curSel_); 448 var earcons = []; 449 450 // Earcons. 451 if (this.skipped_) { 452 earcons.push(cvox.AbstractEarcons.PARAGRAPH_BREAK); 453 this.skipped_ = false; 454 } 455 if (this.recovered_) { 456 earcons.push(cvox.AbstractEarcons.FONT_CHANGE); 457 this.recovered_ = false; 458 } 459 if (this.pageEnd_) { 460 earcons.push(cvox.AbstractEarcons.WRAP); 461 this.pageEnd_ = false; 462 } 463 if (this.enteredShifter_) { 464 earcons.push(cvox.AbstractEarcons.OBJECT_ENTER); 465 this.enteredShifter_ = false; 466 } 467 if (this.exitedShifter_) { 468 earcons.push(cvox.AbstractEarcons.OBJECT_EXIT); 469 this.exitedShifter_ = false; 470 } 471 if (earcons.length > 0 && desc.length > 0) { 472 earcons.forEach(function(earcon) { 473 desc[0].pushEarcon(earcon); 474 }); 475 } 476 return desc; 477}; 478 479 480/** 481 * Delegates to NavigationShifter with the current page state. 482 * @return {!cvox.NavBraille} The braille description. 483 */ 484cvox.NavigationManager.prototype.getBraille = function() { 485 return this.shifter_.getBraille(this.prevSel_, this.curSel_); 486}; 487 488/** 489 * Delegates an action to the current walker. 490 * @param {string} name Action name. 491 * @return {boolean} True if action performed. 492 */ 493cvox.NavigationManager.prototype.performAction = function(name) { 494 var newSel = null; 495 switch (name) { 496 case 'enterShifter': 497 case 'enterShifterSilently': 498 for (var i = this.shifterTypes_.length - 1, shifterType; 499 shifterType = this.shifterTypes_[i]; 500 i--) { 501 var shifter = shifterType.create(this.curSel_); 502 if (shifter && shifter.getName() != this.shifter_.getName()) { 503 this.shifterStack_.push(this.shifter_); 504 this.shifter_ = shifter; 505 this.sync(); 506 this.enteredShifter_ = name != 'enterShifterSilently'; 507 break; 508 } else if (shifter && this.shifter_.getName() == shifter.getName()) { 509 break; 510 } 511 } 512 break; 513 case 'exitShifter': 514 if (this.shifterStack_.length == 0) { 515 return false; 516 } 517 this.shifter_ = this.shifterStack_.pop(); 518 this.sync(); 519 this.exitedShifter_ = true; 520 break; 521 case 'exitShifterContent': 522 if (this.shifterStack_.length == 0) { 523 return false; 524 } 525 this.updateSel(this.shifter_.performAction(name, this.curSel_)); 526 this.shifter_ = this.shifterStack_.pop() || this.shifter_; 527 this.sync(); 528 this.exitedShifter_ = true; 529 break; 530 default: 531 if (this.shifter_.hasAction(name)) { 532 return this.updateSel( 533 this.shifter_.performAction(name, this.curSel_)); 534 } else { 535 return false; 536 } 537 } 538 return true; 539}; 540 541 542/** 543 * Returns the current navigation strategy. 544 * 545 * @return {string} The name of the strategy used. 546 */ 547cvox.NavigationManager.prototype.getGranularityMsg = function() { 548 return this.shifter_.getGranularityMsg(); 549}; 550 551 552/** 553 * Delegates to NavigationShifter. 554 * @param {boolean=} opt_persist Persist the granularity to all running tabs; 555 * defaults to true. 556 */ 557cvox.NavigationManager.prototype.makeMoreGranular = function(opt_persist) { 558 this.shifter_.makeMoreGranular(); 559 this.sync(); 560 this.persistGranularity_(opt_persist); 561}; 562 563 564/** 565 * Delegates to current shifter. 566 * @param {boolean=} opt_persist Persist the granularity to all running tabs; 567 * defaults to true. 568 */ 569cvox.NavigationManager.prototype.makeLessGranular = function(opt_persist) { 570 this.shifter_.makeLessGranular(); 571 this.sync(); 572 this.persistGranularity_(opt_persist); 573}; 574 575 576/** 577 * Delegates to navigation shifter. Behavior is not defined if granularity 578 * was not previously gotten from a call to getGranularity(). This method is 579 * only supported by NavigationShifter which exposes a random access 580 * iterator-like interface. The caller has the option to force granularity 581 which results in exiting any entered shifters. If not forced, and there has 582 * been a shifter entered, setting granularity is a no-op. 583 * @param {number} granularity The desired granularity. 584 * @param {boolean=} opt_force Forces current shifter to NavigationShifter; 585 * false by default. 586 * @param {boolean=} opt_persist Persists setting to all running tabs; defaults 587 * to false. 588 */ 589cvox.NavigationManager.prototype.setGranularity = function( 590 granularity, opt_force, opt_persist) { 591 if (!opt_force && this.shifterStack_.length > 0) { 592 return; 593 } 594 this.shifter_ = this.shifterStack_.shift() || this.shifter_; 595 this.shifters_ = []; 596 this.shifter_.setGranularity(granularity); 597 this.persistGranularity_(opt_persist); 598}; 599 600 601/** 602 * Delegates to NavigationShifter. 603 * @return {number} The current granularity. 604 */ 605cvox.NavigationManager.prototype.getGranularity = function() { 606 var shifter = this.shifterStack_[0] || this.shifter_; 607 return shifter.getGranularity(); 608}; 609 610 611/** 612 * Delegates to NavigationShifter. 613 */ 614cvox.NavigationManager.prototype.ensureSubnavigating = function() { 615 if (!this.shifter_.isSubnavigating()) { 616 this.shifter_.ensureSubnavigating(); 617 this.sync(); 618 } 619}; 620 621 622/** 623 * Stops subnavigating, specifying that we should navigate at a less granular 624 * level than the current navigation strategy. 625 */ 626cvox.NavigationManager.prototype.ensureNotSubnavigating = function() { 627 if (this.shifter_.isSubnavigating()) { 628 this.shifter_.ensureNotSubnavigating(); 629 this.sync(); 630 } 631}; 632 633 634/** 635 * Delegates to NavigationSpeaker. 636 * @param {Array.<cvox.NavDescription>} descriptionArray The array of 637 * NavDescriptions to speak. 638 * @param {number} initialQueueMode The initial queue mode. 639 * @param {Function} completionFunction Function to call when finished speaking. 640 * @param {Object=} opt_personality Optional personality for all descriptions. 641 * @param {string=} opt_category Optional category for all descriptions. 642 */ 643cvox.NavigationManager.prototype.speakDescriptionArray = function( 644 descriptionArray, 645 initialQueueMode, 646 completionFunction, 647 opt_personality, 648 opt_category) { 649 if (opt_personality) { 650 descriptionArray.forEach(function(desc) { 651 if (!desc.personality) { 652 desc.personality = opt_personality; 653 } 654 }); 655 } 656 if (opt_category) { 657 descriptionArray.forEach(function(desc) { 658 if (!desc.category) { 659 desc.category = opt_category; 660 } 661 }); 662 } 663 664 this.navSpeaker_.speakDescriptionArray( 665 descriptionArray, initialQueueMode, completionFunction); 666}; 667 668/** 669 * Add the position of the node on the page. 670 * @param {Node} node The node that ChromeVox should update the position. 671 */ 672cvox.NavigationManager.prototype.updatePosition = function(node) { 673 var msg = cvox.ChromeVox.position; 674 msg[document.location.href] = 675 cvox.DomUtil.elementToPoint(node); 676 677 cvox.ChromeVox.host.sendToBackgroundPage({ 678 'target': 'Prefs', 679 'action': 'setPref', 680 'pref': 'position', 681 'value': JSON.stringify(msg) 682 }); 683}; 684 685 686// TODO(stoarca): The stuff below belongs in its own layer. 687/** 688 * Perform all of the actions that should happen at the end of any 689 * navigation operation: update the lens, play earcons, and speak the 690 * description of the object that was navigated to. 691 * 692 * @param {string=} opt_prefix The string to be prepended to what 693 * is spoken to the user. 694 * @param {boolean=} opt_setFocus Whether or not to focus the current node. 695 * Defaults to true. 696 * @param {number=} opt_queueMode Initial queue mode to use. 697 * @param {function(): ?=} opt_callback Function to call after speaking. 698 */ 699cvox.NavigationManager.prototype.finishNavCommand = function( 700 opt_prefix, opt_setFocus, opt_queueMode, opt_callback) { 701 if (this.pageEnd_ && !this.pageEndAnnounced_) { 702 this.pageEndAnnounced_ = true; 703 cvox.ChromeVox.tts.stop(); 704 cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP); 705 if (cvox.ChromeVox.verbosity === cvox.VERBOSITY_VERBOSE) { 706 var msg = cvox.ChromeVox.msgs.getMsg('wrapped_to_top'); 707 if (this.isReversed()) { 708 msg = cvox.ChromeVox.msgs.getMsg('wrapped_to_bottom'); 709 } 710 cvox.ChromeVox.tts.speak(msg, cvox.AbstractTts.QUEUE_MODE_QUEUE, 711 cvox.AbstractTts.PERSONALITY_ANNOTATION); 712 } 713 return; 714 } 715 716 if (this.enteredShifter_ || this.exitedShifter_) { 717 opt_prefix = cvox.ChromeVox.msgs.getMsg( 718 'enter_content_say', [this.shifter_.getName()]); 719 } 720 721 var descriptionArray = cvox.ChromeVox.navigationManager.getDescription(); 722 723 opt_setFocus = opt_setFocus === undefined ? true : opt_setFocus; 724 725 if (opt_setFocus) { 726 this.setFocus(); 727 } 728 this.updateIndicator(); 729 730 var queueMode = opt_queueMode || cvox.AbstractTts.QUEUE_MODE_FLUSH; 731 732 if (opt_prefix) { 733 cvox.ChromeVox.tts.speak( 734 opt_prefix, queueMode, cvox.AbstractTts.PERSONALITY_ANNOTATION); 735 queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE; 736 } 737 this.speakDescriptionArray(descriptionArray, 738 queueMode, 739 opt_callback || null, 740 null, 741 'nav'); 742 743 this.getBraille().write(); 744 745 this.updatePosition(this.getCurrentNode()); 746}; 747 748 749/** 750 * Moves forward. Stops any subnavigation. 751 * @param {boolean=} opt_ignoreIframes Ignore iframes when navigating. Defaults 752 * to not ignore iframes. 753 * @param {number=} opt_granularity Optionally, switches to granularity before 754 * navigation. 755 * @return {boolean} False if end of document reached. 756 */ 757cvox.NavigationManager.prototype.navigate = function( 758 opt_ignoreIframes, opt_granularity) { 759 this.pageEndAnnounced_ = false; 760 if (this.pageEnd_) { 761 this.pageEnd_ = false; 762 this.syncToBeginning(opt_ignoreIframes); 763 return true; 764 } 765 if (!this.resolve()) { 766 return false; 767 } 768 this.ensureNotSubnavigating(); 769 if (opt_granularity !== undefined && 770 (opt_granularity !== this.getGranularity() || 771 this.shifterStack_.length > 0)) { 772 this.setGranularity(opt_granularity, true); 773 this.sync(); 774 } 775 return this.next_(!opt_ignoreIframes); 776}; 777 778 779/** 780 * Moves forward after switching to a lower granularity until the next 781 * call to navigate(). 782 */ 783cvox.NavigationManager.prototype.subnavigate = function() { 784 this.pageEndAnnounced_ = false; 785 if (!this.resolve()) { 786 return; 787 } 788 this.ensureSubnavigating(); 789 this.next_(true); 790}; 791 792 793/** 794 * Moves forward. Starts reading the page from that node. 795 * Uses QUEUE_MODE_FLUSH to flush any previous speech. 796 * @return {boolean} False if not "reading from here". True otherwise. 797 */ 798cvox.NavigationManager.prototype.skip = function() { 799 if (!this.keepReading_) { 800 return false; 801 } 802 if (cvox.ChromeVox.host.hasTtsCallback()) { 803 this.skipped_ = true; 804 this.setReversed(false); 805 this.startCallbackReading_(cvox.AbstractTts.QUEUE_MODE_FLUSH); 806 } 807 return true; 808}; 809 810 811/** 812 * Starts reading the page from the current selection. 813 * @param {number} queueMode Either flush or queue. 814 */ 815cvox.NavigationManager.prototype.startReading = function(queueMode) { 816 this.keepReading_ = true; 817 if (cvox.ChromeVox.host.hasTtsCallback()) { 818 this.startCallbackReading_(queueMode); 819 } else { 820 this.startNonCallbackReading_(queueMode); 821 } 822 cvox.ChromeVox.stickyOverride = true; 823}; 824 825/** 826 * Stops continuous read. 827 * @param {boolean} stopTtsImmediately True if the TTS should immediately stop 828 * speaking. 829 */ 830cvox.NavigationManager.prototype.stopReading = function(stopTtsImmediately) { 831 this.keepReading_ = false; 832 this.navSpeaker_.stopReading = true; 833 if (stopTtsImmediately) { 834 cvox.ChromeVox.tts.stop(); 835 } 836 cvox.ChromeVox.stickyOverride = null; 837}; 838 839 840/** 841 * The current current state of continuous read. 842 * @return {boolean} The state. 843 */ 844cvox.NavigationManager.prototype.isReading = function() { 845 return this.keepReading_; 846}; 847 848 849/** 850 * Starts reading the page from the current selection if there are callbacks. 851 * @param {number} queueMode Either flush or queue. 852 * @private 853 */ 854cvox.NavigationManager.prototype.startCallbackReading_ = 855 cvox.ChromeVoxEventSuspender.withSuspendedEvents(function(queueMode) { 856 this.finishNavCommand('', true, queueMode, goog.bind(function() { 857 if (this.next_(true) && this.keepReading_) { 858 this.startCallbackReading_(cvox.AbstractTts.QUEUE_MODE_QUEUE); 859 } 860 }, this)); 861}); 862 863 864/** 865 * Starts reading the page from the current selection if there are no callbacks. 866 * With this method, we poll the keepReading_ var and stop when it is false. 867 * @param {number} queueMode Either flush or queue. 868 * @private 869 */ 870cvox.NavigationManager.prototype.startNonCallbackReading_ = 871 cvox.ChromeVoxEventSuspender.withSuspendedEvents(function(queueMode) { 872 if (!this.keepReading_) { 873 return; 874 } 875 876 if (!cvox.ChromeVox.tts.isSpeaking()) { 877 this.finishNavCommand('', true, queueMode, null); 878 if (!this.next_(true)) { 879 this.keepReading_ = false; 880 } 881 } 882 window.setTimeout(goog.bind(this.startNonCallbackReading_, this), 1000); 883}); 884 885 886/** 887 * Returns a complete description of the current position, including 888 * the text content and annotations such as "link", "button", etc. 889 * Unlike getDescription, this does not shorten the position based on the 890 * previous position. 891 * 892 * @return {Array.<cvox.NavDescription>} The summary of the current position. 893 */ 894cvox.NavigationManager.prototype.getFullDescription = function() { 895 if (this.pageSel_) { 896 return this.pageSel_.getFullDescription(); 897 } 898 return [cvox.DescriptionUtil.getDescriptionFromAncestors( 899 cvox.DomUtil.getAncestors(this.curSel_.start.node), 900 true, 901 cvox.ChromeVox.verbosity)]; 902}; 903 904 905/** 906 * Sets the browser's focus to the current node. 907 * @param {boolean=} opt_skipText Skips focusing text nodes or any of their 908 * ancestors; defaults to false. 909 */ 910cvox.NavigationManager.prototype.setFocus = function(opt_skipText) { 911 // TODO(dtseng): cvox.DomUtil.setFocus() totally destroys DOM ranges that have 912 // been set on the page; this requires further investigation, but 913 // PageSelection won't work without this. 914 if (this.pageSel_ || 915 (opt_skipText && this.curSel_.start.node.constructor == Text)) { 916 return; 917 } 918 cvox.Focuser.setFocus(this.curSel_.start.node); 919}; 920 921 922/** 923 * Returns the node of the directed start of the selection. 924 * @return {Node} The current node. 925 */ 926cvox.NavigationManager.prototype.getCurrentNode = function() { 927 return this.curSel_.absStart().node; 928}; 929 930 931/** 932 * Listen to messages from other frames and respond to messages that 933 * tell our frame to take focus and preseve the navigation granularity 934 * from the other frame. 935 * @private 936 */ 937cvox.NavigationManager.prototype.addInterframeListener_ = function() { 938 /** 939 * @type {!cvox.NavigationManager} 940 */ 941 var self = this; 942 943 cvox.Interframe.addListener(function(message) { 944 if (message['command'] != 'enterIframe' && 945 message['command'] != 'exitIframe') { 946 return; 947 } 948 cvox.ChromeVox.serializer.readFrom(message); 949 if (self.keepReading_) { 950 return; 951 } 952 cvox.ChromeVoxEventSuspender.withSuspendedEvents(function() { 953 window.focus(); 954 955 if (message['findNext']) { 956 var predicateName = message['findNext']; 957 var predicate = cvox.DomPredicates[predicateName]; 958 var found = self.findNext(predicate, predicateName, true); 959 if (predicate && (!found || found.start.node.tagName == 'IFRAME')) { 960 return; 961 } 962 } else if (message['command'] == 'exitIframe') { 963 var id = message['sourceId']; 964 var iframeElement = self.iframeIdMap[id]; 965 var reversed = message['reversed']; 966 var granularity = message['granularity']; 967 if (iframeElement) { 968 self.updateSel(cvox.CursorSelection.fromNode(iframeElement)); 969 } 970 self.setReversed(reversed); 971 self.sync(); 972 self.navigate(); 973 } else { 974 self.syncToBeginning(); 975 976 // if we have an empty body, then immediately exit the iframe 977 if (!cvox.DomUtil.hasContent(document.body)) { 978 self.tryIframe_(null); 979 return; 980 } 981 } 982 983 // Now speak what ended up being selected. 984 // TODO(deboer): Some of this could be moved to readFrom 985 self.finishNavCommand('', true); 986 })(); 987 }); 988}; 989 990 991/** 992 * Update the active indicator to reflect the current node or selection. 993 */ 994cvox.NavigationManager.prototype.updateIndicator = function() { 995 this.activeIndicator.syncToCursorSelection(this.curSel_); 996}; 997 998 999/** 1000 * Update the active indicator in case the active object moved or was 1001 * removed from the document. 1002 */ 1003cvox.NavigationManager.prototype.updateIndicatorIfChanged = function() { 1004 this.activeIndicator.updateIndicatorIfChanged(); 1005}; 1006 1007 1008/** 1009 * Show or hide the active indicator based on whether ChromeVox is 1010 * active or not. 1011 * 1012 * If 'active' is true, cvox.NavigationManager does not do anything. 1013 * However, callers to showOrHideIndicator also need to call updateIndicator 1014 * to update the indicator -- which also does the work to show the 1015 * indicator. 1016 * 1017 * @param {boolean} active True if we should show the indicator, false 1018 * if we should hide the indicator. 1019 */ 1020cvox.NavigationManager.prototype.showOrHideIndicator = function(active) { 1021 if (!active) { 1022 this.activeIndicator.removeFromDom(); 1023 } 1024}; 1025 1026 1027/** 1028 * Collapses the selection to directed cursor start. 1029 */ 1030cvox.NavigationManager.prototype.collapseSelection = function() { 1031 this.curSel_.collapse(); 1032}; 1033 1034 1035/** 1036 * This is used to update the selection to arbitrary nodes because there are 1037 * browser events, cvox API's, and user commands that require selection around a 1038 * precise node. As a consequence, calling this method will result in a shift to 1039 * object granularity without explicit user action or feedback. Also, note that 1040 * this selection will be sync'ed to ObjectWalker by default unless explicitly 1041 * ttold not to. We assume object walker can describe the node in the latter 1042 * case. 1043 * @param {Node} node The node to update to. 1044 * @param {boolean=} opt_precise Whether selection will sync exactly to the 1045 * given node. Defaults to false (and selection will sync according to object 1046 * walker). 1047 */ 1048cvox.NavigationManager.prototype.updateSelToArbitraryNode = function( 1049 node, opt_precise) { 1050 if (node) { 1051 this.setGranularity(cvox.NavigationShifter.GRANULARITIES.OBJECT, true); 1052 this.updateSel(cvox.CursorSelection.fromNode(node)); 1053 if (!opt_precise) { 1054 this.sync(); 1055 } 1056 } else { 1057 this.syncToBeginning(); 1058 } 1059}; 1060 1061 1062/** 1063 * Updates curSel_ to the new selection and sets prevSel_ to the old curSel_. 1064 * This should be called exactly when something user-perceivable happens. 1065 * @param {cvox.CursorSelection} sel The selection to update to. 1066 * @param {cvox.CursorSelection=} opt_context An optional override for prevSel_. 1067 * Used to override both curSel_ and prevSel_ when jumping back in nav history. 1068 * @return {boolean} False if sel is null. True otherwise. 1069 */ 1070cvox.NavigationManager.prototype.updateSel = function(sel, opt_context) { 1071 if (sel) { 1072 this.prevSel_ = opt_context || this.curSel_; 1073 this.curSel_ = sel; 1074 } 1075 // Only update the history if we aren't just trying to peek ahead. 1076 var currentNode = this.getCurrentNode(); 1077 this.navigationHistory_.update(currentNode); 1078 return !!sel; 1079}; 1080 1081 1082/** 1083 * Sets the direction. 1084 * @param {!boolean} r True to reverse. 1085 */ 1086cvox.NavigationManager.prototype.setReversed = function(r) { 1087 this.curSel_.setReversed(r); 1088}; 1089 1090 1091/** 1092 * Returns true if currently reversed. 1093 * @return {boolean} True if reversed. 1094 */ 1095cvox.NavigationManager.prototype.isReversed = function() { 1096 return this.curSel_.isReversed(); 1097}; 1098 1099 1100/** 1101 * Checks if boundary conditions are met and updates the selection. 1102 * @param {cvox.CursorSelection} sel The selection. 1103 * @param {boolean=} iframes If true, tries to enter iframes. Default false. 1104 * @return {boolean} False if end of page is reached. 1105 * @private 1106 */ 1107cvox.NavigationManager.prototype.tryBoundaries_ = function(sel, iframes) { 1108 iframes = (!!iframes && !this.ignoreIframesNoMatterWhat_) || false; 1109 this.pageEnd_ = false; 1110 if (iframes && this.tryIframe_(sel && sel.start.node)) { 1111 return true; 1112 } 1113 if (sel) { 1114 this.updateSel(sel); 1115 return true; 1116 } 1117 if (this.shifterStack_.length > 0) { 1118 return true; 1119 } 1120 this.syncToBeginning(!iframes); 1121 this.clearPageSel(true); 1122 this.stopReading(true); 1123 this.pageEnd_ = true; 1124 return false; 1125}; 1126 1127 1128/** 1129 * Given a node that we just navigated to, try to jump in and out of iframes 1130 * as needed. If the node is an iframe, jump into it. If the node is null, 1131 * assume we reached the end of an iframe and try to jump out of it. 1132 * @param {Node} node The node to try to jump into. 1133 * @return {boolean} True if we jumped into an iframe. 1134 * @private 1135 */ 1136cvox.NavigationManager.prototype.tryIframe_ = function(node) { 1137 if (node == null && cvox.Interframe.isIframe()) { 1138 var message = { 1139 'command': 'exitIframe', 1140 'reversed': this.isReversed(), 1141 'granularity': this.getGranularity() 1142 }; 1143 cvox.ChromeVox.serializer.storeOn(message); 1144 cvox.Interframe.sendMessageToParentWindow(message); 1145 return true; 1146 } 1147 1148 if (node == null || node.tagName != 'IFRAME' || !node.src) { 1149 return false; 1150 } 1151 var iframeElement = /** @type {HTMLIFrameElement} */(node); 1152 1153 var iframeId = undefined; 1154 for (var id in this.iframeIdMap) { 1155 if (this.iframeIdMap[id] == iframeElement) { 1156 iframeId = id; 1157 break; 1158 } 1159 } 1160 if (iframeId == undefined) { 1161 iframeId = this.nextIframeId; 1162 this.nextIframeId++; 1163 this.iframeIdMap[iframeId] = iframeElement; 1164 cvox.Interframe.sendIdToIFrame(iframeId, iframeElement); 1165 } 1166 1167 var message = { 1168 'command': 'enterIframe', 1169 'id': iframeId 1170 }; 1171 cvox.ChromeVox.serializer.storeOn(message); 1172 cvox.Interframe.sendMessageToIFrame(message, iframeElement); 1173 1174 return true; 1175}; 1176 1177 1178/** 1179 * Delegates to NavigationShifter. Tries to enter any iframes or tables if 1180 * requested. 1181 * @param {boolean=} opt_skipIframe True to skip iframes. 1182 */ 1183cvox.NavigationManager.prototype.syncToBeginning = function(opt_skipIframe) { 1184 var ret = this.shifter_.begin(this.curSel_, { 1185 reversed: this.curSel_.isReversed() 1186 }); 1187 if (!opt_skipIframe && this.tryIframe_(ret && ret.start.node)) { 1188 return; 1189 } 1190 this.updateSel(ret); 1191}; 1192 1193 1194/** 1195 * Used during testing since there are iframes and we don't always want to 1196 * interact with them so that we can test certain features. 1197 */ 1198cvox.NavigationManager.prototype.ignoreIframesNoMatterWhat = function() { 1199 this.ignoreIframesNoMatterWhat_ = true; 1200}; 1201 1202 1203/** 1204 * Save a cursor selection during an excursion. 1205 */ 1206cvox.NavigationManager.prototype.saveSel = function() { 1207 this.saveSel_ = this.curSel_; 1208}; 1209 1210 1211/** 1212 * Save a cursor selection after an excursion. 1213 */ 1214cvox.NavigationManager.prototype.restoreSel = function() { 1215 this.curSel_ = this.saveSel_ || this.curSel_; 1216}; 1217 1218 1219/** 1220 * @param {boolean=} opt_persist Persist the granularity to all running tabs; 1221 * defaults to false. 1222 * @private 1223 */ 1224cvox.NavigationManager.prototype.persistGranularity_ = function(opt_persist) { 1225 opt_persist = opt_persist === undefined ? false : opt_persist; 1226 if (opt_persist) { 1227 cvox.ChromeVox.host.sendToBackgroundPage({ 1228 'target': 'Prefs', 1229 'action': 'setPref', 1230 'pref': 'granularity', 1231 'value': this.getGranularity() 1232 }); 1233 } 1234}; 1235