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