diff options
Diffstat (limited to 'chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/navigation_manager.js')
-rw-r--r-- | chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/navigation_manager.js | 1234 |
1 files changed, 1234 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/navigation_manager.js b/chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/navigation_manager.js new file mode 100644 index 00000000000..a7236cddd27 --- /dev/null +++ b/chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/navigation_manager.js @@ -0,0 +1,1234 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview Manages navigation within a page. + * This unifies navigation by the DOM walker and by WebKit selection. + * NOTE: the purpose of this class is only to hold state + * and delegate all of its functionality to mostly stateless classes that + * are easy to test. + * + */ + + +goog.provide('cvox.NavigationManager'); + +goog.require('cvox.ActiveIndicator'); +goog.require('cvox.ChromeVox'); +goog.require('cvox.ChromeVoxEventSuspender'); +goog.require('cvox.CursorSelection'); +goog.require('cvox.DescriptionUtil'); +goog.require('cvox.DomUtil'); +goog.require('cvox.FindUtil'); +goog.require('cvox.Focuser'); +goog.require('cvox.Interframe'); +goog.require('cvox.MathShifter'); +goog.require('cvox.NavBraille'); +goog.require('cvox.NavDescription'); +goog.require('cvox.NavigationHistory'); +goog.require('cvox.NavigationShifter'); +goog.require('cvox.NavigationSpeaker'); +goog.require('cvox.PageSelection'); +goog.require('cvox.SelectionUtil'); +goog.require('cvox.TableShifter'); +goog.require('cvox.TraverseMath'); +goog.require('cvox.Widget'); + + +/** + * @constructor + */ +cvox.NavigationManager = function() { + this.addInterframeListener_(); + + this.reset(); +}; + +/** + * Stores state variables in a provided object. + * + * @param {Object} store The object. + */ +cvox.NavigationManager.prototype.storeOn = function(store) { + store['reversed'] = this.isReversed(); + store['keepReading'] = this.keepReading_; + store['findNext'] = this.predicate_; + this.shifter_.storeOn(store); +}; + +/** + * Updates the object with state variables from an earlier storeOn call. + * + * @param {Object} store The object. + */ +cvox.NavigationManager.prototype.readFrom = function(store) { + this.curSel_.setReversed(store['reversed']); + this.shifter_.readFrom(store); + if (store['keepReading']) { + this.startReading(cvox.AbstractTts.QUEUE_MODE_FLUSH); + } +}; + +/** + * Resets the navigation manager to the top of the page. + */ +cvox.NavigationManager.prototype.reset = function() { + /** + * @type {!cvox.NavigationSpeaker} + * @private + */ + this.navSpeaker_ = new cvox.NavigationSpeaker(); + + /** + * @type {!Array.<Object>} + * @private + */ + this.shifterTypes_ = [cvox.NavigationShifter, + cvox.TableShifter, + cvox.MathShifter]; + + /** + * @type {!Array.<!cvox.AbstractShifter>} + */ + this.shifterStack_ = []; + + /** + * The active shifter. + * @type {!cvox.AbstractShifter} + * @private + */ + this.shifter_ = new cvox.NavigationShifter(); + + // NOTE(deboer): document.activeElement can not be null (c.f. + // https://developer.mozilla.org/en-US/docs/DOM/document.activeElement) + // Instead, if there is no active element, activeElement is set to + // document.body. + /** + * If there is an activeElement, use it. Otherwise, sync to the page + * beginning. + * @type {!cvox.CursorSelection} + * @private + */ + this.curSel_ = document.activeElement != document.body ? + /** @type {!cvox.CursorSelection} **/ + (cvox.CursorSelection.fromNode(document.activeElement)) : + this.shifter_.begin(this.curSel_, {reversed: false}); + + /** + * @type {!cvox.CursorSelection} + * @private + */ + this.prevSel_ = this.curSel_.clone(); + + /** + * Keeps track of whether we have skipped while "reading from here" + * so that we can insert an earcon. + * @type {boolean} + * @private + */ + this.skipped_ = false; + + /** + * Keeps track of whether we have recovered from dropped focus + * so that we can insert an earcon. + * @type {boolean} + * @private + */ + this.recovered_ = false; + + /** + * True if in "reading from here" mode. + * @type {boolean} + * @private + */ + this.keepReading_ = false; + + /** + * True if we are at the end of the page and we wrap around. + * @type {boolean} + * @private + */ + this.pageEnd_ = false; + + /** + * True if we have already announced that we will wrap around. + * @type {boolean} + * @private + */ + this.pageEndAnnounced_ = false; + + /** + * True if we entered into a shifter. + * @type {boolean} + * @private + */ + this.enteredShifter_ = false; + + /** + * True if we exited a shifter. + * @type {boolean} + * @private + */ + this.exitedShifter_ = false; + + /** + * True if we want to ignore iframes no matter what. + * @type {boolean} + * @private + */ + this.ignoreIframesNoMatterWhat_ = false; + + /** + * @type {cvox.PageSelection} + * @private + */ + this.pageSel_ = null; + + /** @type {string} */ + this.predicate_ = ''; + + /** @type {cvox.CursorSelection} */ + this.saveSel_ = null; + + // TODO(stoarca): This seems goofy. Why are we doing this? + if (this.activeIndicator) { + this.activeIndicator.removeFromDom(); + } + this.activeIndicator = new cvox.ActiveIndicator(); + + /** + * Makes sure focus doesn't get lost. + * @type {!cvox.NavigationHistory} + * @private + */ + this.navigationHistory_ = new cvox.NavigationHistory(); + + /** @type {boolean} */ + this.focusRecovery_ = window.location.protocol != 'chrome:'; + + this.iframeIdMap = {}; + this.nextIframeId = 1; + + // Only sync if the activeElement is not document.body; which is shorthand for + // 'no selection'. Currently the walkers don't deal with the no selection + // case -- and it is not clear that they should. + if (document.activeElement != document.body) { + this.sync(); + } + + // This object is effectively empty when no math is in the page. + cvox.TraverseMath.getInstance(); +}; + + +/** + * Determines if we are navigating from a valid node. If not, ask navigation + * history for an acceptable restart point and go there. + * @param {function(Node)=} opt_predicate A function that takes in a node and + * returns true if it is a valid recovery candidate. + * @return {boolean} True if we should continue navigation normally. + */ +cvox.NavigationManager.prototype.resolve = function(opt_predicate) { + if (!this.getFocusRecovery()) { + return true; + } + + var current = this.getCurrentNode(); + + if (!this.navigationHistory_.becomeInvalid(current)) { + return true; + } + + // Only attempt to revert if going next will cause us to restart at the top + // of the page. + if (this.hasNext_()) { + return true; + } + + // Our current node was invalid. Revert to history. + var revert = this.navigationHistory_.revert(opt_predicate); + + // If the history is empty, revert.current will be null. In that case, + // it is best to continue navigating normally. + if (!revert.current) { + return true; + } + + // Convert to selections. + var newSel = cvox.CursorSelection.fromNode(revert.current); + var context = cvox.CursorSelection.fromNode(revert.previous); + + // Default to document body if selections are null. + newSel = newSel || cvox.CursorSelection.fromBody(); + context = context || cvox.CursorSelection.fromBody(); + newSel.setReversed(this.isReversed()); + + this.updateSel(newSel, context); + this.recovered_ = true; + return false; +}; + + +/** + * Gets the state of focus recovery. + * @return {boolean} True if focus recovery is on; false otherwise. + */ +cvox.NavigationManager.prototype.getFocusRecovery = function() { + return this.focusRecovery_; +}; + + +/** + * Enables or disables focus recovery. + * @param {boolean} value True to enable, false to disable. + */ +cvox.NavigationManager.prototype.setFocusRecovery = function(value) { + this.focusRecovery_ = value; +}; + + +/** + * Delegates to NavigationShifter with current page state. + * @param {boolean=} iframes Jump in and out of iframes if true. Default false. + * @return {boolean} False if end of document has been reached. + * @private + */ +cvox.NavigationManager.prototype.next_ = function(iframes) { + if (this.tryBoundaries_(this.shifter_.next(this.curSel_), iframes)) { + // TODO(dtseng): An observer interface would help to keep logic like this + // to a minimum. + this.pageSel_ && this.pageSel_.extend(this.curSel_); + return true; + } + return false; +}; + +/** + * Looks ahead to see if it is possible to navigate forward from the current + * position. + * @return {boolean} True if it is possible to navigate forward. + * @private + */ +cvox.NavigationManager.prototype.hasNext_ = function() { + // Non-default shifters validly end before page end. + if (this.shifterStack_.length > 0) { + return true; + } + var dummySel = this.curSel_.clone(); + var result = false; + var dummyNavShifter = new cvox.NavigationShifter(); + dummyNavShifter.setGranularity(this.shifter_.getGranularity()); + dummyNavShifter.sync(dummySel); + if (dummyNavShifter.next(dummySel)) { + result = true; + } + return result; +}; + + +/** + * Delegates to NavigationShifter with current page state. + * @param {function(Array.<Node>)} predicate A function taking an array + * of unique ancestor nodes as a parameter and returning a desired node. + * It returns null if that node can't be found. + * @param {string=} opt_predicateName The programmatic name that exists in + * cvox.DomPredicates. Used to dispatch calls across iframes since functions + * cannot be stringified. + * @param {boolean=} opt_initialNode Whether to start the search from node + * (true), or the next node (false); defaults to false. + * @return {cvox.CursorSelection} The newly found selection. + */ +cvox.NavigationManager.prototype.findNext = function( + predicate, opt_predicateName, opt_initialNode) { + this.predicate_ = opt_predicateName || ''; + this.resolve(); + this.shifter_ = this.shifterStack_[0] || this.shifter_; + this.shifterStack_ = []; + var ret = cvox.FindUtil.findNext(this.curSel_, predicate, opt_initialNode); + if (!this.ignoreIframesNoMatterWhat_) { + this.tryIframe_(ret && ret.start.node); + } + if (ret) { + this.updateSelToArbitraryNode(ret.start.node); + } + this.predicate_ = ''; + return ret; +}; + + +/** + * Delegates to NavigationShifter with current page state. + */ +cvox.NavigationManager.prototype.sync = function() { + this.resolve(); + var ret = this.shifter_.sync(this.curSel_); + if (ret) { + this.curSel_ = ret; + } +}; + +/** + * Sync's all possible cursors: + * - focus + * - ActiveIndicator + * - CursorSelection + * @param {boolean=} opt_skipText Skips focus on text nodes; defaults to false. + */ +cvox.NavigationManager.prototype.syncAll = function(opt_skipText) { + this.sync(); + this.setFocus(opt_skipText); + this.updateIndicator(); +}; + + +/** + * Clears a DOM selection made via a CursorSelection. + * @param {boolean=} opt_announce True to announce the clearing. + * @return {boolean} If a selection was cleared. + */ +cvox.NavigationManager.prototype.clearPageSel = function(opt_announce) { + var hasSel = !!this.pageSel_; + if (hasSel && opt_announce) { + var announcement = cvox.ChromeVox.msgs.getMsg('clear_page_selection'); + cvox.ChromeVox.tts.speak(announcement, cvox.AbstractTts.QUEUE_MODE_FLUSH, + cvox.AbstractTts.PERSONALITY_ANNOTATION); + } + this.pageSel_ = null; + return hasSel; +}; + + +/** + * Begins or finishes a DOM selection at the current CursorSelection in the + * document. + * @return {boolean} Whether selection is on or off after this call. + */ +cvox.NavigationManager.prototype.togglePageSel = function() { + this.pageSel_ = this.pageSel_ ? null : + new cvox.PageSelection(this.curSel_.setReversed(false)); + return !!this.pageSel_; +}; + + +// TODO(stoarca): getDiscription is split awkwardly between here and the +// walkers. The walkers should have getBaseDescription() which requires +// very little context, and then this method should tack on everything +// which requires any extensive knowledge. +/** + * Delegates to NavigationShifter with the current page state. + * @return {Array.<cvox.NavDescription>} The summary of the current position. + */ +cvox.NavigationManager.prototype.getDescription = function() { + // Handle description of special content. Consider moving to DescriptionUtil. + // Specially annotated nodes. + if (this.getCurrentNode().hasAttribute && + this.getCurrentNode().hasAttribute('cvoxnodedesc')) { + var preDesc = cvox.ChromeVoxJSON.parse( + this.getCurrentNode().getAttribute('cvoxnodedesc')); + var currentDesc = new Array(); + for (var i = 0; i < preDesc.length; ++i) { + var inDesc = preDesc[i]; + // TODO: this can probably be replaced with just NavDescription(inDesc) + // need test case to ensure this change will work + currentDesc.push(new cvox.NavDescription({ + context: inDesc.context, + text: inDesc.text, + userValue: inDesc.userValue, + annotation: inDesc.annotation + })); + } + return currentDesc; + } + + // Selected content. + var desc = this.pageSel_ ? this.pageSel_.getDescription( + this.shifter_, this.prevSel_, this.curSel_) : + this.shifter_.getDescription(this.prevSel_, this.curSel_); + var earcons = []; + + // Earcons. + if (this.skipped_) { + earcons.push(cvox.AbstractEarcons.PARAGRAPH_BREAK); + this.skipped_ = false; + } + if (this.recovered_) { + earcons.push(cvox.AbstractEarcons.FONT_CHANGE); + this.recovered_ = false; + } + if (this.pageEnd_) { + earcons.push(cvox.AbstractEarcons.WRAP); + this.pageEnd_ = false; + } + if (this.enteredShifter_) { + earcons.push(cvox.AbstractEarcons.OBJECT_ENTER); + this.enteredShifter_ = false; + } + if (this.exitedShifter_) { + earcons.push(cvox.AbstractEarcons.OBJECT_EXIT); + this.exitedShifter_ = false; + } + if (earcons.length > 0 && desc.length > 0) { + earcons.forEach(function(earcon) { + desc[0].pushEarcon(earcon); + }); + } + return desc; +}; + + +/** + * Delegates to NavigationShifter with the current page state. + * @return {!cvox.NavBraille} The braille description. + */ +cvox.NavigationManager.prototype.getBraille = function() { + return this.shifter_.getBraille(this.prevSel_, this.curSel_); +}; + +/** + * Delegates an action to the current walker. + * @param {string} name Action name. + * @return {boolean} True if action performed. + */ +cvox.NavigationManager.prototype.performAction = function(name) { + var newSel = null; + switch (name) { + case 'enterShifter': + case 'enterShifterSilently': + for (var i = this.shifterTypes_.length - 1, shifterType; + shifterType = this.shifterTypes_[i]; + i--) { + var shifter = shifterType.create(this.curSel_); + if (shifter && shifter.getName() != this.shifter_.getName()) { + this.shifterStack_.push(this.shifter_); + this.shifter_ = shifter; + this.sync(); + this.enteredShifter_ = name != 'enterShifterSilently'; + break; + } else if (shifter && this.shifter_.getName() == shifter.getName()) { + break; + } + } + break; + case 'exitShifter': + if (this.shifterStack_.length == 0) { + return false; + } + this.shifter_ = this.shifterStack_.pop(); + this.sync(); + this.exitedShifter_ = true; + break; + case 'exitShifterContent': + if (this.shifterStack_.length == 0) { + return false; + } + this.updateSel(this.shifter_.performAction(name, this.curSel_)); + this.shifter_ = this.shifterStack_.pop() || this.shifter_; + this.sync(); + this.exitedShifter_ = true; + break; + default: + if (this.shifter_.hasAction(name)) { + return this.updateSel( + this.shifter_.performAction(name, this.curSel_)); + } else { + return false; + } + } + return true; +}; + + +/** + * Returns the current navigation strategy. + * + * @return {string} The name of the strategy used. + */ +cvox.NavigationManager.prototype.getGranularityMsg = function() { + return this.shifter_.getGranularityMsg(); +}; + + +/** + * Delegates to NavigationShifter. + * @param {boolean=} opt_persist Persist the granularity to all running tabs; + * defaults to true. + */ +cvox.NavigationManager.prototype.makeMoreGranular = function(opt_persist) { + this.shifter_.makeMoreGranular(); + this.sync(); + this.persistGranularity_(opt_persist); +}; + + +/** + * Delegates to current shifter. + * @param {boolean=} opt_persist Persist the granularity to all running tabs; + * defaults to true. + */ +cvox.NavigationManager.prototype.makeLessGranular = function(opt_persist) { + this.shifter_.makeLessGranular(); + this.sync(); + this.persistGranularity_(opt_persist); +}; + + +/** + * Delegates to navigation shifter. Behavior is not defined if granularity + * was not previously gotten from a call to getGranularity(). This method is + * only supported by NavigationShifter which exposes a random access + * iterator-like interface. The caller has the option to force granularity + which results in exiting any entered shifters. If not forced, and there has + * been a shifter entered, setting granularity is a no-op. + * @param {number} granularity The desired granularity. + * @param {boolean=} opt_force Forces current shifter to NavigationShifter; + * false by default. + * @param {boolean=} opt_persist Persists setting to all running tabs; defaults + * to false. + */ +cvox.NavigationManager.prototype.setGranularity = function( + granularity, opt_force, opt_persist) { + if (!opt_force && this.shifterStack_.length > 0) { + return; + } + this.shifter_ = this.shifterStack_.shift() || this.shifter_; + this.shifters_ = []; + this.shifter_.setGranularity(granularity); + this.persistGranularity_(opt_persist); +}; + + +/** + * Delegates to NavigationShifter. + * @return {number} The current granularity. + */ +cvox.NavigationManager.prototype.getGranularity = function() { + var shifter = this.shifterStack_[0] || this.shifter_; + return shifter.getGranularity(); +}; + + +/** + * Delegates to NavigationShifter. + */ +cvox.NavigationManager.prototype.ensureSubnavigating = function() { + if (!this.shifter_.isSubnavigating()) { + this.shifter_.ensureSubnavigating(); + this.sync(); + } +}; + + +/** + * Stops subnavigating, specifying that we should navigate at a less granular + * level than the current navigation strategy. + */ +cvox.NavigationManager.prototype.ensureNotSubnavigating = function() { + if (this.shifter_.isSubnavigating()) { + this.shifter_.ensureNotSubnavigating(); + this.sync(); + } +}; + + +/** + * Delegates to NavigationSpeaker. + * @param {Array.<cvox.NavDescription>} descriptionArray The array of + * NavDescriptions to speak. + * @param {number} initialQueueMode The initial queue mode. + * @param {Function} completionFunction Function to call when finished speaking. + * @param {Object=} opt_personality Optional personality for all descriptions. + * @param {string=} opt_category Optional category for all descriptions. + */ +cvox.NavigationManager.prototype.speakDescriptionArray = function( + descriptionArray, + initialQueueMode, + completionFunction, + opt_personality, + opt_category) { + if (opt_personality) { + descriptionArray.every(function(desc) { + if (!desc.personality) { + desc.personality = opt_personality; + } + }); + } + if (opt_category) { + descriptionArray.every(function(desc) { + if (!desc.category) { + desc.category = opt_category; + } + }); + } + + this.navSpeaker_.speakDescriptionArray( + descriptionArray, initialQueueMode, completionFunction); +}; + +/** + * Add the position of the node on the page. + * @param {Node} node The node that ChromeVox should update the position. + */ +cvox.NavigationManager.prototype.updatePosition = function(node) { + var msg = cvox.ChromeVox.position; + msg[document.location.href] = + cvox.DomUtil.elementToPoint(node); + + cvox.ChromeVox.host.sendToBackgroundPage({ + 'target': 'Prefs', + 'action': 'setPref', + 'pref': 'position', + 'value': JSON.stringify(msg) + }); +}; + + +// TODO(stoarca): The stuff below belongs in its own layer. +/** + * Perform all of the actions that should happen at the end of any + * navigation operation: update the lens, play earcons, and speak the + * description of the object that was navigated to. + * + * @param {string=} opt_prefix The string to be prepended to what + * is spoken to the user. + * @param {boolean=} opt_setFocus Whether or not to focus the current node. + * Defaults to true. + * @param {number=} opt_queueMode Initial queue mode to use. + * @param {function(): ?=} opt_callback Function to call after speaking. + */ +cvox.NavigationManager.prototype.finishNavCommand = function( + opt_prefix, opt_setFocus, opt_queueMode, opt_callback) { + if (this.pageEnd_ && !this.pageEndAnnounced_) { + this.pageEndAnnounced_ = true; + cvox.ChromeVox.tts.stop(); + cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP); + if (cvox.ChromeVox.verbosity === cvox.VERBOSITY_VERBOSE) { + var msg = cvox.ChromeVox.msgs.getMsg('wrapped_to_top'); + if (this.isReversed()) { + msg = cvox.ChromeVox.msgs.getMsg('wrapped_to_bottom'); + } + cvox.ChromeVox.tts.speak(msg, cvox.AbstractTts.QUEUE_MODE_QUEUE, + cvox.AbstractTts.PERSONALITY_ANNOTATION); + } + return; + } + + if (this.enteredShifter_ || this.exitedShifter_) { + opt_prefix = cvox.ChromeVox.msgs.getMsg( + 'enter_content_say', [this.shifter_.getName()]); + } + + var descriptionArray = cvox.ChromeVox.navigationManager.getDescription(); + + opt_setFocus = opt_setFocus === undefined ? true : opt_setFocus; + + if (opt_setFocus) { + this.setFocus(); + } + this.updateIndicator(); + + var queueMode = opt_queueMode || cvox.AbstractTts.QUEUE_MODE_FLUSH; + + if (opt_prefix) { + cvox.ChromeVox.tts.speak( + opt_prefix, queueMode, cvox.AbstractTts.PERSONALITY_ANNOTATION); + queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE; + } + this.speakDescriptionArray(descriptionArray, + queueMode, + opt_callback || null, + null, + 'nav'); + + this.getBraille().write(); + + this.updatePosition(this.getCurrentNode()); +}; + + +/** + * Moves forward. Stops any subnavigation. + * @param {boolean=} opt_ignoreIframes Ignore iframes when navigating. Defaults + * to not ignore iframes. + * @param {number=} opt_granularity Optionally, switches to granularity before + * navigation. + * @return {boolean} False if end of document reached. + */ +cvox.NavigationManager.prototype.navigate = function( + opt_ignoreIframes, opt_granularity) { + this.pageEndAnnounced_ = false; + if (this.pageEnd_) { + this.pageEnd_ = false; + this.syncToBeginning(opt_ignoreIframes); + return true; + } + if (!this.resolve()) { + return false; + } + this.ensureNotSubnavigating(); + if (opt_granularity !== undefined && + (opt_granularity !== this.getGranularity() || + this.shifterStack_.length > 0)) { + this.setGranularity(opt_granularity, true); + this.sync(); + } + return this.next_(!opt_ignoreIframes); +}; + + +/** + * Moves forward after switching to a lower granularity until the next + * call to navigate(). + */ +cvox.NavigationManager.prototype.subnavigate = function() { + this.pageEndAnnounced_ = false; + if (!this.resolve()) { + return; + } + this.ensureSubnavigating(); + this.next_(true); +}; + + +/** + * Moves forward. Starts reading the page from that node. + * Uses QUEUE_MODE_FLUSH to flush any previous speech. + * @return {boolean} False if not "reading from here". True otherwise. + */ +cvox.NavigationManager.prototype.skip = function() { + if (!this.keepReading_) { + return false; + } + if (cvox.ChromeVox.host.hasTtsCallback()) { + this.skipped_ = true; + this.setReversed(false); + this.startCallbackReading_(cvox.AbstractTts.QUEUE_MODE_FLUSH); + } + return true; +}; + + +/** + * Starts reading the page from the current selection. + * @param {number} queueMode Either flush or queue. + */ +cvox.NavigationManager.prototype.startReading = function(queueMode) { + this.keepReading_ = true; + if (cvox.ChromeVox.host.hasTtsCallback()) { + this.startCallbackReading_(queueMode); + } else { + this.startNonCallbackReading_(queueMode); + } + cvox.ChromeVox.stickyOverride = true; +}; + +/** + * Stops continuous read. + * @param {boolean} stopTtsImmediately True if the TTS should immediately stop + * speaking. + */ +cvox.NavigationManager.prototype.stopReading = function(stopTtsImmediately) { + this.keepReading_ = false; + this.navSpeaker_.stopReading = true; + if (stopTtsImmediately) { + cvox.ChromeVox.tts.stop(); + } + cvox.ChromeVox.stickyOverride = null; +}; + + +/** + * The current current state of continuous read. + * @return {boolean} The state. + */ +cvox.NavigationManager.prototype.isReading = function() { + return this.keepReading_; +}; + + +/** + * Starts reading the page from the current selection if there are callbacks. + * @param {number} queueMode Either flush or queue. + * @private + */ +cvox.NavigationManager.prototype.startCallbackReading_ = + cvox.ChromeVoxEventSuspender.withSuspendedEvents(function(queueMode) { + this.finishNavCommand('', true, queueMode, goog.bind(function() { + if (this.next_(true) && this.keepReading_) { + this.startCallbackReading_(cvox.AbstractTts.QUEUE_MODE_QUEUE); + } + }, this)); +}); + + +/** + * Starts reading the page from the current selection if there are no callbacks. + * With this method, we poll the keepReading_ var and stop when it is false. + * @param {number} queueMode Either flush or queue. + * @private + */ +cvox.NavigationManager.prototype.startNonCallbackReading_ = + cvox.ChromeVoxEventSuspender.withSuspendedEvents(function(queueMode) { + if (!this.keepReading_) { + return; + } + + if (!cvox.ChromeVox.tts.isSpeaking()) { + this.finishNavCommand('', true, queueMode, null); + if (!this.next_(true)) { + this.keepReading_ = false; + } + } + window.setTimeout(goog.bind(this.startNonCallbackReading_, this), 1000); +}); + + +/** + * Returns a complete description of the current position, including + * the text content and annotations such as "link", "button", etc. + * Unlike getDescription, this does not shorten the position based on the + * previous position. + * + * @return {Array.<cvox.NavDescription>} The summary of the current position. + */ +cvox.NavigationManager.prototype.getFullDescription = function() { + if (this.pageSel_) { + return this.pageSel_.getFullDescription(); + } + return [cvox.DescriptionUtil.getDescriptionFromAncestors( + cvox.DomUtil.getAncestors(this.curSel_.start.node), + true, + cvox.ChromeVox.verbosity)]; +}; + + +/** + * Sets the browser's focus to the current node. + * @param {boolean=} opt_skipText Skips focusing text nodes or any of their + * ancestors; defaults to false. + */ +cvox.NavigationManager.prototype.setFocus = function(opt_skipText) { + // TODO(dtseng): cvox.DomUtil.setFocus() totally destroys DOM ranges that have + // been set on the page; this requires further investigation, but + // PageSelection won't work without this. + if (this.pageSel_ || + (opt_skipText && this.curSel_.start.node.constructor == Text)) { + return; + } + cvox.Focuser.setFocus(this.curSel_.start.node); +}; + + +/** + * Returns the node of the directed start of the selection. + * @return {Node} The current node. + */ +cvox.NavigationManager.prototype.getCurrentNode = function() { + return this.curSel_.absStart().node; +}; + + +/** + * Listen to messages from other frames and respond to messages that + * tell our frame to take focus and preseve the navigation granularity + * from the other frame. + * @private + */ +cvox.NavigationManager.prototype.addInterframeListener_ = function() { + /** + * @type {!cvox.NavigationManager} + */ + var self = this; + + cvox.Interframe.addListener(function(message) { + if (message['command'] != 'enterIframe' && + message['command'] != 'exitIframe') { + return; + } + cvox.ChromeVox.serializer.readFrom(message); + if (self.keepReading_) { + return; + } + cvox.ChromeVoxEventSuspender.withSuspendedEvents(function() { + window.focus(); + + if (message['findNext']) { + var predicateName = message['findNext']; + var predicate = cvox.DomPredicates[predicateName]; + var found = self.findNext(predicate, predicateName, true); + if (predicate && (!found || found.start.node.tagName == 'IFRAME')) { + return; + } + } else if (message['command'] == 'exitIframe') { + var id = message['sourceId']; + var iframeElement = self.iframeIdMap[id]; + var reversed = message['reversed']; + var granularity = message['granularity']; + if (iframeElement) { + self.updateSel(cvox.CursorSelection.fromNode(iframeElement)); + } + self.setReversed(reversed); + self.sync(); + self.navigate(); + } else { + self.syncToBeginning(); + + // if we have an empty body, then immediately exit the iframe + if (!cvox.DomUtil.hasContent(document.body)) { + self.tryIframe_(null); + return; + } + } + + // Now speak what ended up being selected. + // TODO(deboer): Some of this could be moved to readFrom + self.finishNavCommand('', true); + })(); + }); +}; + + +/** + * Update the active indicator to reflect the current node or selection. + */ +cvox.NavigationManager.prototype.updateIndicator = function() { + this.activeIndicator.syncToCursorSelection(this.curSel_); +}; + + +/** + * Update the active indicator in case the active object moved or was + * removed from the document. + */ +cvox.NavigationManager.prototype.updateIndicatorIfChanged = function() { + this.activeIndicator.updateIndicatorIfChanged(); +}; + + +/** + * Show or hide the active indicator based on whether ChromeVox is + * active or not. + * + * If 'active' is true, cvox.NavigationManager does not do anything. + * However, callers to showOrHideIndicator also need to call updateIndicator + * to update the indicator -- which also does the work to show the + * indicator. + * + * @param {boolean} active True if we should show the indicator, false + * if we should hide the indicator. + */ +cvox.NavigationManager.prototype.showOrHideIndicator = function(active) { + if (!active) { + this.activeIndicator.removeFromDom(); + } +}; + + +/** + * Collapses the selection to directed cursor start. + */ +cvox.NavigationManager.prototype.collapseSelection = function() { + this.curSel_.collapse(); +}; + + +/** + * This is used to update the selection to arbitrary nodes because there are + * browser events, cvox API's, and user commands that require selection around a + * precise node. As a consequence, calling this method will result in a shift to + * object granularity without explicit user action or feedback. Also, note that + * this selection will be sync'ed to ObjectWalker by default unless explicitly + * ttold not to. We assume object walker can describe the node in the latter + * case. + * @param {Node} node The node to update to. + * @param {boolean=} opt_precise Whether selection will sync exactly to the + * given node. Defaults to false (and selection will sync according to object + * walker). + */ +cvox.NavigationManager.prototype.updateSelToArbitraryNode = function( + node, opt_precise) { + if (node) { + this.setGranularity(cvox.NavigationShifter.GRANULARITIES.OBJECT, true); + this.updateSel(cvox.CursorSelection.fromNode(node)); + if (!opt_precise) { + this.sync(); + } + } else { + this.syncToBeginning(); + } +}; + + +/** + * Updates curSel_ to the new selection and sets prevSel_ to the old curSel_. + * This should be called exactly when something user-perceivable happens. + * @param {cvox.CursorSelection} sel The selection to update to. + * @param {cvox.CursorSelection=} opt_context An optional override for prevSel_. + * Used to override both curSel_ and prevSel_ when jumping back in nav history. + * @return {boolean} False if sel is null. True otherwise. + */ +cvox.NavigationManager.prototype.updateSel = function(sel, opt_context) { + if (sel) { + this.prevSel_ = opt_context || this.curSel_; + this.curSel_ = sel; + } + // Only update the history if we aren't just trying to peek ahead. + var currentNode = this.getCurrentNode(); + this.navigationHistory_.update(currentNode); + return !!sel; +}; + + +/** + * Sets the direction. + * @param {!boolean} r True to reverse. + */ +cvox.NavigationManager.prototype.setReversed = function(r) { + this.curSel_.setReversed(r); +}; + + +/** + * Returns true if currently reversed. + * @return {boolean} True if reversed. + */ +cvox.NavigationManager.prototype.isReversed = function() { + return this.curSel_.isReversed(); +}; + + +/** + * Checks if boundary conditions are met and updates the selection. + * @param {cvox.CursorSelection} sel The selection. + * @param {boolean=} iframes If true, tries to enter iframes. Default false. + * @return {boolean} False if end of page is reached. + * @private + */ +cvox.NavigationManager.prototype.tryBoundaries_ = function(sel, iframes) { + iframes = (!!iframes && !this.ignoreIframesNoMatterWhat_) || false; + this.pageEnd_ = false; + if (iframes && this.tryIframe_(sel && sel.start.node)) { + return true; + } + if (sel) { + this.updateSel(sel); + return true; + } + if (this.shifterStack_.length > 0) { + return true; + } + this.syncToBeginning(!iframes); + this.clearPageSel(true); + this.stopReading(true); + this.pageEnd_ = true; + return false; +}; + + +/** + * Given a node that we just navigated to, try to jump in and out of iframes + * as needed. If the node is an iframe, jump into it. If the node is null, + * assume we reached the end of an iframe and try to jump out of it. + * @param {Node} node The node to try to jump into. + * @return {boolean} True if we jumped into an iframe. + * @private + */ +cvox.NavigationManager.prototype.tryIframe_ = function(node) { + if (node == null && cvox.Interframe.isIframe()) { + var message = { + 'command': 'exitIframe', + 'reversed': this.isReversed(), + 'granularity': this.getGranularity() + }; + cvox.ChromeVox.serializer.storeOn(message); + cvox.Interframe.sendMessageToParentWindow(message); + return true; + } + + if (node == null || node.tagName != 'IFRAME' || !node.src) { + return false; + } + var iframeElement = /** @type {HTMLIFrameElement} */(node); + + var iframeId = undefined; + for (var id in this.iframeIdMap) { + if (this.iframeIdMap[id] == iframeElement) { + iframeId = id; + break; + } + } + if (iframeId == undefined) { + iframeId = this.nextIframeId; + this.nextIframeId++; + this.iframeIdMap[iframeId] = iframeElement; + cvox.Interframe.sendIdToIFrame(iframeId, iframeElement); + } + + var message = { + 'command': 'enterIframe', + 'id': iframeId + }; + cvox.ChromeVox.serializer.storeOn(message); + cvox.Interframe.sendMessageToIFrame(message, iframeElement); + + return true; +}; + + +/** + * Delegates to NavigationShifter. Tries to enter any iframes or tables if + * requested. + * @param {boolean=} opt_skipIframe True to skip iframes. + */ +cvox.NavigationManager.prototype.syncToBeginning = function(opt_skipIframe) { + var ret = this.shifter_.begin(this.curSel_, { + reversed: this.curSel_.isReversed() + }); + if (!opt_skipIframe && this.tryIframe_(ret && ret.start.node)) { + return; + } + this.updateSel(ret); +}; + + +/** + * Used during testing since there are iframes and we don't always want to + * interact with them so that we can test certain features. + */ +cvox.NavigationManager.prototype.ignoreIframesNoMatterWhat = function() { + this.ignoreIframesNoMatterWhat_ = true; +}; + + +/** + * Save a cursor selection during an excursion. + */ +cvox.NavigationManager.prototype.saveSel = function() { + this.saveSel_ = this.curSel_; +}; + + +/** + * Save a cursor selection after an excursion. + */ +cvox.NavigationManager.prototype.restoreSel = function() { + this.curSel_ = this.saveSel_ || this.curSel_; +}; + + +/** + * @param {boolean=} opt_persist Persist the granularity to all running tabs; + * defaults to false. + * @private + */ +cvox.NavigationManager.prototype.persistGranularity_ = function(opt_persist) { + opt_persist = opt_persist === undefined ? false : opt_persist; + if (opt_persist) { + cvox.ChromeVox.host.sendToBackgroundPage({ + 'target': 'Prefs', + 'action': 'setPref', + 'pref': 'granularity', + 'value': this.getGranularity() + }); + } +}; |