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.
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
15 goog.provide('cvox.NavigationManager');
17 goog.require('cvox.ActiveIndicator');
18 goog.require('cvox.ChromeVox');
19 goog.require('cvox.ChromeVoxEventSuspender');
20 goog.require('cvox.CursorSelection');
21 goog.require('cvox.DescriptionUtil');
22 goog.require('cvox.DomUtil');
23 goog.require('cvox.FindUtil');
24 goog.require('cvox.Focuser');
25 goog.require('cvox.Interframe');
26 goog.require('cvox.MathShifter');
27 goog.require('cvox.NavBraille');
28 goog.require('cvox.NavDescription');
29 goog.require('cvox.NavigationHistory');
30 goog.require('cvox.NavigationShifter');
31 goog.require('cvox.NavigationSpeaker');
32 goog.require('cvox.PageSelection');
33 goog.require('cvox.SelectionUtil');
34 goog.require('cvox.TableShifter');
35 goog.require('cvox.TraverseMath');
36 goog.require('cvox.Widget');
42 cvox.NavigationManager = function() {
43 this.addInterframeListener_();
49 * Stores state variables in a provided object.
51 * @param {Object} store The object.
53 cvox.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);
61 * Updates the object with state variables from an earlier storeOn call.
63 * @param {Object} store The object.
65 cvox.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);
74 * Resets the navigation manager to the top of the page.
76 cvox.NavigationManager.prototype.reset = function() {
78 * @type {!cvox.NavigationSpeaker}
81 this.navSpeaker_ = new cvox.NavigationSpeaker();
84 * @type {!Array.<Object>}
87 this.shifterTypes_ = [cvox.NavigationShifter,
92 * @type {!Array.<!cvox.AbstractShifter>}
94 this.shifterStack_ = [];
98 * @type {!cvox.AbstractShifter}
101 this.shifter_ = new cvox.NavigationShifter();
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
108 * If there is an activeElement, use it. Otherwise, sync to the page
110 * @type {!cvox.CursorSelection}
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});
119 * @type {!cvox.CursorSelection}
122 this.prevSel_ = this.curSel_.clone();
125 * Keeps track of whether we have skipped while "reading from here"
126 * so that we can insert an earcon.
130 this.skipped_ = false;
133 * Keeps track of whether we have recovered from dropped focus
134 * so that we can insert an earcon.
138 this.recovered_ = false;
141 * True if in "reading from here" mode.
145 this.keepReading_ = false;
148 * True if we are at the end of the page and we wrap around.
152 this.pageEnd_ = false;
155 * True if we have already announced that we will wrap around.
159 this.pageEndAnnounced_ = false;
162 * True if we entered into a shifter.
166 this.enteredShifter_ = false;
169 * True if we exited a shifter.
173 this.exitedShifter_ = false;
176 * True if we want to ignore iframes no matter what.
180 this.ignoreIframesNoMatterWhat_ = false;
183 * @type {cvox.PageSelection}
186 this.pageSel_ = null;
188 /** @type {string} */
189 this.predicate_ = '';
191 /** @type {cvox.CursorSelection} */
192 this.saveSel_ = null;
194 // TODO(stoarca): This seems goofy. Why are we doing this?
195 if (this.activeIndicator) {
196 this.activeIndicator.removeFromDom();
198 this.activeIndicator = new cvox.ActiveIndicator();
201 * Makes sure focus doesn't get lost.
202 * @type {!cvox.NavigationHistory}
205 this.navigationHistory_ = new cvox.NavigationHistory();
207 /** @type {boolean} */
208 this.focusRecovery_ = window.location.protocol != 'chrome:';
210 this.iframeIdMap = {};
211 this.nextIframeId = 1;
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) {
220 // This object is effectively empty when no math is in the page.
221 cvox.TraverseMath.getInstance();
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.
232 cvox.NavigationManager.prototype.resolve = function(opt_predicate) {
233 if (!this.getFocusRecovery()) {
237 var current = this.getCurrentNode();
239 if (!this.navigationHistory_.becomeInvalid(current)) {
243 // Only attempt to revert if going next will cause us to restart at the top
245 if (this.hasNext_()) {
249 // Our current node was invalid. Revert to history.
250 var revert = this.navigationHistory_.revert(opt_predicate);
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) {
258 // Convert to selections.
259 var newSel = cvox.CursorSelection.fromNode(revert.current);
260 var context = cvox.CursorSelection.fromNode(revert.previous);
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());
267 this.updateSel(newSel, context);
268 this.recovered_ = true;
274 * Gets the state of focus recovery.
275 * @return {boolean} True if focus recovery is on; false otherwise.
277 cvox.NavigationManager.prototype.getFocusRecovery = function() {
278 return this.focusRecovery_;
283 * Enables or disables focus recovery.
284 * @param {boolean} value True to enable, false to disable.
286 cvox.NavigationManager.prototype.setFocusRecovery = function(value) {
287 this.focusRecovery_ = value;
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.
297 cvox.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
301 this.pageSel_ && this.pageSel_.extend(this.curSel_);
308 * Looks ahead to see if it is possible to navigate forward from the current
310 * @return {boolean} True if it is possible to navigate forward.
313 cvox.NavigationManager.prototype.hasNext_ = function() {
314 // Non-default shifters validly end before page end.
315 if (this.shifterStack_.length > 0) {
318 var dummySel = this.curSel_.clone();
320 var dummyNavShifter = new cvox.NavigationShifter();
321 dummyNavShifter.setGranularity(this.shifter_.getGranularity());
322 dummyNavShifter.sync(dummySel);
323 if (dummyNavShifter.next(dummySel)) {
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.
342 cvox.NavigationManager.prototype.findNext = function(
343 predicate, opt_predicateName, opt_initialNode) {
344 this.predicate_ = opt_predicateName || '';
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);
353 this.updateSelToArbitraryNode(ret.start.node);
355 this.predicate_ = '';
361 * Delegates to NavigationShifter with current page state.
363 cvox.NavigationManager.prototype.sync = function() {
365 var ret = this.shifter_.sync(this.curSel_);
372 * Sync's all possible cursors:
376 * @param {boolean=} opt_skipText Skips focus on text nodes; defaults to false.
378 cvox.NavigationManager.prototype.syncAll = function(opt_skipText) {
380 this.setFocus(opt_skipText);
381 this.updateIndicator();
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.
390 cvox.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);
397 this.pageSel_ = null;
403 * Begins or finishes a DOM selection at the current CursorSelection in the
405 * @return {boolean} Whether selection is on or off after this call.
407 cvox.NavigationManager.prototype.togglePageSel = function() {
408 this.pageSel_ = this.pageSel_ ? null :
409 new cvox.PageSelection(this.curSel_.setReversed(false));
410 return !!this.pageSel_;
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.
419 * Delegates to NavigationShifter with the current page state.
420 * @return {Array.<cvox.NavDescription>} The summary of the current position.
422 cvox.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,
437 userValue: inDesc.userValue,
438 annotation: inDesc.annotation
445 var desc = this.pageSel_ ? this.pageSel_.getDescription(
446 this.shifter_, this.prevSel_, this.curSel_) :
447 this.shifter_.getDescription(this.prevSel_, this.curSel_);
452 earcons.push(cvox.AbstractEarcons.PARAGRAPH_BREAK);
453 this.skipped_ = false;
455 if (this.recovered_) {
456 earcons.push(cvox.AbstractEarcons.FONT_CHANGE);
457 this.recovered_ = false;
460 earcons.push(cvox.AbstractEarcons.WRAP);
461 this.pageEnd_ = false;
463 if (this.enteredShifter_) {
464 earcons.push(cvox.AbstractEarcons.OBJECT_ENTER);
465 this.enteredShifter_ = false;
467 if (this.exitedShifter_) {
468 earcons.push(cvox.AbstractEarcons.OBJECT_EXIT);
469 this.exitedShifter_ = false;
471 if (earcons.length > 0 && desc.length > 0) {
472 earcons.forEach(function(earcon) {
473 desc[0].pushEarcon(earcon);
481 * Delegates to NavigationShifter with the current page state.
482 * @return {!cvox.NavBraille} The braille description.
484 cvox.NavigationManager.prototype.getBraille = function() {
485 return this.shifter_.getBraille(this.prevSel_, this.curSel_);
489 * Delegates an action to the current walker.
490 * @param {string} name Action name.
491 * @return {boolean} True if action performed.
493 cvox.NavigationManager.prototype.performAction = function(name) {
497 case 'enterShifterSilently':
498 for (var i = this.shifterTypes_.length - 1, shifterType;
499 shifterType = this.shifterTypes_[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;
506 this.enteredShifter_ = name != 'enterShifterSilently';
508 } else if (shifter && this.shifter_.getName() == shifter.getName()) {
514 if (this.shifterStack_.length == 0) {
517 this.shifter_ = this.shifterStack_.pop();
519 this.exitedShifter_ = true;
521 case 'exitShifterContent':
522 if (this.shifterStack_.length == 0) {
525 this.updateSel(this.shifter_.performAction(name, this.curSel_));
526 this.shifter_ = this.shifterStack_.pop() || this.shifter_;
528 this.exitedShifter_ = true;
531 if (this.shifter_.hasAction(name)) {
532 return this.updateSel(
533 this.shifter_.performAction(name, this.curSel_));
543 * Returns the current navigation strategy.
545 * @return {string} The name of the strategy used.
547 cvox.NavigationManager.prototype.getGranularityMsg = function() {
548 return this.shifter_.getGranularityMsg();
553 * Delegates to NavigationShifter.
554 * @param {boolean=} opt_persist Persist the granularity to all running tabs;
557 cvox.NavigationManager.prototype.makeMoreGranular = function(opt_persist) {
558 this.shifter_.makeMoreGranular();
560 this.persistGranularity_(opt_persist);
565 * Delegates to current shifter.
566 * @param {boolean=} opt_persist Persist the granularity to all running tabs;
569 cvox.NavigationManager.prototype.makeLessGranular = function(opt_persist) {
570 this.shifter_.makeLessGranular();
572 this.persistGranularity_(opt_persist);
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;
586 * @param {boolean=} opt_persist Persists setting to all running tabs; defaults
589 cvox.NavigationManager.prototype.setGranularity = function(
590 granularity, opt_force, opt_persist) {
591 if (!opt_force && this.shifterStack_.length > 0) {
594 this.shifter_ = this.shifterStack_.shift() || this.shifter_;
596 this.shifter_.setGranularity(granularity);
597 this.persistGranularity_(opt_persist);
602 * Delegates to NavigationShifter.
603 * @return {number} The current granularity.
605 cvox.NavigationManager.prototype.getGranularity = function() {
606 var shifter = this.shifterStack_[0] || this.shifter_;
607 return shifter.getGranularity();
612 * Delegates to NavigationShifter.
614 cvox.NavigationManager.prototype.ensureSubnavigating = function() {
615 if (!this.shifter_.isSubnavigating()) {
616 this.shifter_.ensureSubnavigating();
623 * Stops subnavigating, specifying that we should navigate at a less granular
624 * level than the current navigation strategy.
626 cvox.NavigationManager.prototype.ensureNotSubnavigating = function() {
627 if (this.shifter_.isSubnavigating()) {
628 this.shifter_.ensureNotSubnavigating();
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.
643 cvox.NavigationManager.prototype.speakDescriptionArray = function(
649 if (opt_personality) {
650 descriptionArray.forEach(function(desc) {
651 if (!desc.personality) {
652 desc.personality = opt_personality;
657 descriptionArray.forEach(function(desc) {
658 if (!desc.category) {
659 desc.category = opt_category;
664 this.navSpeaker_.speakDescriptionArray(
665 descriptionArray, initialQueueMode, completionFunction);
669 * Add the position of the node on the page.
670 * @param {Node} node The node that ChromeVox should update the position.
672 cvox.NavigationManager.prototype.updatePosition = function(node) {
673 var msg = cvox.ChromeVox.position;
674 msg[document.location.href] =
675 cvox.DomUtil.elementToPoint(node);
677 cvox.ChromeVox.host.sendToBackgroundPage({
681 'value': JSON.stringify(msg)
686 // TODO(stoarca): The stuff below belongs in its own layer.
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.
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.
696 * @param {number=} opt_queueMode Initial queue mode to use.
697 * @param {function(): ?=} opt_callback Function to call after speaking.
699 cvox.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');
710 cvox.ChromeVox.tts.speak(msg, cvox.AbstractTts.QUEUE_MODE_QUEUE,
711 cvox.AbstractTts.PERSONALITY_ANNOTATION);
716 if (this.enteredShifter_ || this.exitedShifter_) {
717 opt_prefix = cvox.ChromeVox.msgs.getMsg(
718 'enter_content_say', [this.shifter_.getName()]);
721 var descriptionArray = cvox.ChromeVox.navigationManager.getDescription();
723 opt_setFocus = opt_setFocus === undefined ? true : opt_setFocus;
728 this.updateIndicator();
730 var queueMode = opt_queueMode || cvox.AbstractTts.QUEUE_MODE_FLUSH;
733 cvox.ChromeVox.tts.speak(
734 opt_prefix, queueMode, cvox.AbstractTts.PERSONALITY_ANNOTATION);
735 queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
737 this.speakDescriptionArray(descriptionArray,
739 opt_callback || null,
743 this.getBraille().write();
745 this.updatePosition(this.getCurrentNode());
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
755 * @return {boolean} False if end of document reached.
757 cvox.NavigationManager.prototype.navigate = function(
758 opt_ignoreIframes, opt_granularity) {
759 this.pageEndAnnounced_ = false;
761 this.pageEnd_ = false;
762 this.syncToBeginning(opt_ignoreIframes);
765 if (!this.resolve()) {
768 this.ensureNotSubnavigating();
769 if (opt_granularity !== undefined &&
770 (opt_granularity !== this.getGranularity() ||
771 this.shifterStack_.length > 0)) {
772 this.setGranularity(opt_granularity, true);
775 return this.next_(!opt_ignoreIframes);
780 * Moves forward after switching to a lower granularity until the next
781 * call to navigate().
783 cvox.NavigationManager.prototype.subnavigate = function() {
784 this.pageEndAnnounced_ = false;
785 if (!this.resolve()) {
788 this.ensureSubnavigating();
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.
798 cvox.NavigationManager.prototype.skip = function() {
799 if (!this.keepReading_) {
802 if (cvox.ChromeVox.host.hasTtsCallback()) {
803 this.skipped_ = true;
804 this.setReversed(false);
805 this.startCallbackReading_(cvox.AbstractTts.QUEUE_MODE_FLUSH);
812 * Starts reading the page from the current selection.
813 * @param {number} queueMode Either flush or queue.
815 cvox.NavigationManager.prototype.startReading = function(queueMode) {
816 this.keepReading_ = true;
817 if (cvox.ChromeVox.host.hasTtsCallback()) {
818 this.startCallbackReading_(queueMode);
820 this.startNonCallbackReading_(queueMode);
822 cvox.ChromeVox.stickyOverride = true;
826 * Stops continuous read.
827 * @param {boolean} stopTtsImmediately True if the TTS should immediately stop
830 cvox.NavigationManager.prototype.stopReading = function(stopTtsImmediately) {
831 this.keepReading_ = false;
832 this.navSpeaker_.stopReading = true;
833 if (stopTtsImmediately) {
834 cvox.ChromeVox.tts.stop();
836 cvox.ChromeVox.stickyOverride = null;
841 * The current current state of continuous read.
842 * @return {boolean} The state.
844 cvox.NavigationManager.prototype.isReading = function() {
845 return this.keepReading_;
850 * Starts reading the page from the current selection if there are callbacks.
851 * @param {number} queueMode Either flush or queue.
854 cvox.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);
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.
870 cvox.NavigationManager.prototype.startNonCallbackReading_ =
871 cvox.ChromeVoxEventSuspender.withSuspendedEvents(function(queueMode) {
872 if (!this.keepReading_) {
876 if (!cvox.ChromeVox.tts.isSpeaking()) {
877 this.finishNavCommand('', true, queueMode, null);
878 if (!this.next_(true)) {
879 this.keepReading_ = false;
882 window.setTimeout(goog.bind(this.startNonCallbackReading_, this), 1000);
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
892 * @return {Array.<cvox.NavDescription>} The summary of the current position.
894 cvox.NavigationManager.prototype.getFullDescription = function() {
896 return this.pageSel_.getFullDescription();
898 return [cvox.DescriptionUtil.getDescriptionFromAncestors(
899 cvox.DomUtil.getAncestors(this.curSel_.start.node),
901 cvox.ChromeVox.verbosity)];
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.
910 cvox.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.
915 (opt_skipText && this.curSel_.start.node.constructor == Text)) {
918 cvox.Focuser.setFocus(this.curSel_.start.node);
923 * Returns the node of the directed start of the selection.
924 * @return {Node} The current node.
926 cvox.NavigationManager.prototype.getCurrentNode = function() {
927 return this.curSel_.absStart().node;
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.
937 cvox.NavigationManager.prototype.addInterframeListener_ = function() {
939 * @type {!cvox.NavigationManager}
943 cvox.Interframe.addListener(function(message) {
944 if (message['command'] != 'enterIframe' &&
945 message['command'] != 'exitIframe') {
948 cvox.ChromeVox.serializer.readFrom(message);
949 if (self.keepReading_) {
952 cvox.ChromeVoxEventSuspender.withSuspendedEvents(function() {
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')) {
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'];
968 self.updateSel(cvox.CursorSelection.fromNode(iframeElement));
970 self.setReversed(reversed);
974 self.syncToBeginning();
976 // if we have an empty body, then immediately exit the iframe
977 if (!cvox.DomUtil.hasContent(document.body)) {
978 self.tryIframe_(null);
983 // Now speak what ended up being selected.
984 // TODO(deboer): Some of this could be moved to readFrom
985 self.finishNavCommand('', true);
992 * Update the active indicator to reflect the current node or selection.
994 cvox.NavigationManager.prototype.updateIndicator = function() {
995 this.activeIndicator.syncToCursorSelection(this.curSel_);
1000 * Update the active indicator in case the active object moved or was
1001 * removed from the document.
1003 cvox.NavigationManager.prototype.updateIndicatorIfChanged = function() {
1004 this.activeIndicator.updateIndicatorIfChanged();
1009 * Show or hide the active indicator based on whether ChromeVox is
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
1017 * @param {boolean} active True if we should show the indicator, false
1018 * if we should hide the indicator.
1020 cvox.NavigationManager.prototype.showOrHideIndicator = function(active) {
1022 this.activeIndicator.removeFromDom();
1028 * Collapses the selection to directed cursor start.
1030 cvox.NavigationManager.prototype.collapseSelection = function() {
1031 this.curSel_.collapse();
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
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
1048 cvox.NavigationManager.prototype.updateSelToArbitraryNode = function(
1049 node, opt_precise) {
1051 this.setGranularity(cvox.NavigationShifter.GRANULARITIES.OBJECT, true);
1052 this.updateSel(cvox.CursorSelection.fromNode(node));
1057 this.syncToBeginning();
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.
1070 cvox.NavigationManager.prototype.updateSel = function(sel, opt_context) {
1072 this.prevSel_ = opt_context || this.curSel_;
1075 // Only update the history if we aren't just trying to peek ahead.
1076 var currentNode = this.getCurrentNode();
1077 this.navigationHistory_.update(currentNode);
1083 * Sets the direction.
1084 * @param {!boolean} r True to reverse.
1086 cvox.NavigationManager.prototype.setReversed = function(r) {
1087 this.curSel_.setReversed(r);
1092 * Returns true if currently reversed.
1093 * @return {boolean} True if reversed.
1095 cvox.NavigationManager.prototype.isReversed = function() {
1096 return this.curSel_.isReversed();
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.
1107 cvox.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)) {
1114 this.updateSel(sel);
1117 if (this.shifterStack_.length > 0) {
1120 this.syncToBeginning(!iframes);
1121 this.clearPageSel(true);
1122 this.stopReading(true);
1123 this.pageEnd_ = true;
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.
1136 cvox.NavigationManager.prototype.tryIframe_ = function(node) {
1137 if (node == null && cvox.Interframe.isIframe()) {
1139 'command': 'exitIframe',
1140 'reversed': this.isReversed(),
1141 'granularity': this.getGranularity()
1143 cvox.ChromeVox.serializer.storeOn(message);
1144 cvox.Interframe.sendMessageToParentWindow(message);
1148 if (node == null || node.tagName != 'IFRAME' || !node.src) {
1151 var iframeElement = /** @type {HTMLIFrameElement} */(node);
1153 var iframeId = undefined;
1154 for (var id in this.iframeIdMap) {
1155 if (this.iframeIdMap[id] == iframeElement) {
1160 if (iframeId == undefined) {
1161 iframeId = this.nextIframeId;
1162 this.nextIframeId++;
1163 this.iframeIdMap[iframeId] = iframeElement;
1164 cvox.Interframe.sendIdToIFrame(iframeId, iframeElement);
1168 'command': 'enterIframe',
1171 cvox.ChromeVox.serializer.storeOn(message);
1172 cvox.Interframe.sendMessageToIFrame(message, iframeElement);
1179 * Delegates to NavigationShifter. Tries to enter any iframes or tables if
1181 * @param {boolean=} opt_skipIframe True to skip iframes.
1183 cvox.NavigationManager.prototype.syncToBeginning = function(opt_skipIframe) {
1184 var ret = this.shifter_.begin(this.curSel_, {
1185 reversed: this.curSel_.isReversed()
1187 if (!opt_skipIframe && this.tryIframe_(ret && ret.start.node)) {
1190 this.updateSel(ret);
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.
1198 cvox.NavigationManager.prototype.ignoreIframesNoMatterWhat = function() {
1199 this.ignoreIframesNoMatterWhat_ = true;
1204 * Save a cursor selection during an excursion.
1206 cvox.NavigationManager.prototype.saveSel = function() {
1207 this.saveSel_ = this.curSel_;
1212 * Save a cursor selection after an excursion.
1214 cvox.NavigationManager.prototype.restoreSel = function() {
1215 this.curSel_ = this.saveSel_ || this.curSel_;
1220 * @param {boolean=} opt_persist Persist the granularity to all running tabs;
1221 * defaults to false.
1224 cvox.NavigationManager.prototype.persistGranularity_ = function(opt_persist) {
1225 opt_persist = opt_persist === undefined ? false : opt_persist;
1227 cvox.ChromeVox.host.sendToBackgroundPage({
1229 'action': 'setPref',
1230 'pref': 'granularity',
1231 'value': this.getGranularity()