blob: 2ec204e1ff4cd10c8634e40e3e893466cc80be6d [file] [log] [blame]
// 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.QueueMode.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.QueueMode.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 {cvox.QueueMode} 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.forEach(function(desc) {
if (!desc.personality) {
desc.personality = opt_personality;
}
});
}
if (opt_category) {
descriptionArray.forEach(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 {cvox.QueueMode=} 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.QueueMode.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.QueueMode.FLUSH;
if (opt_prefix) {
cvox.ChromeVox.tts.speak(
opt_prefix, queueMode, cvox.AbstractTts.PERSONALITY_ANNOTATION);
queueMode = cvox.QueueMode.QUEUE;
}
this.speakDescriptionArray(descriptionArray,
queueMode,
opt_callback || null,
null,
cvox.TtsCategory.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.QueueMode.FLUSH);
}
return true;
};
/**
* Starts reading the page from the current selection.
* @param {cvox.QueueMode} 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 {cvox.QueueMode} 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.QueueMode.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 {cvox.QueueMode} 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()
});
}
};