Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / chromeos / chromevox / chromevox / injected / navigation_manager.js
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
15 goog.provide('cvox.NavigationManager');
16
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');
37
38
39 /**
40  * @constructor
41  */
42 cvox.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  */
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);
58 };
59
60 /**
61  * Updates the object with state variables from an earlier storeOn call.
62  *
63  * @param {Object} store The object.
64  */
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);
70   }
71 };
72
73 /**
74  * Resets the navigation manager to the top of the page.
75  */
76 cvox.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  */
232 cvox.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  */
277 cvox.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  */
286 cvox.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  */
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
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  */
313 cvox.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  */
342 cvox.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  */
363 cvox.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  */
378 cvox.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  */
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);
396   }
397   this.pageSel_ = null;
398   return hasSel;
399 };
400
401
402 /**
403  * Begins or finishes a DOM selection at the current CursorSelection in the
404  * document.
405  * @return {boolean} Whether selection is on or off after this call.
406  */
407 cvox.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  */
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,
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  */
484 cvox.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  */
493 cvox.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  */
547 cvox.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  */
557 cvox.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  */
569 cvox.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  */
589 cvox.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  */
605 cvox.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  */
614 cvox.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  */
626 cvox.NavigationManager.prototype.ensureNotSubnavigating = function() {
627   if (this.shifter_.isSubnavigating()) {
628     this.shifter_.ensureNotSubnavigating();
629     this.sync();
630   }
631 };
632
633
634 /**
635  * Delegates to NavigationSpeaker.
636  * @param {Array.<cvox.NavDescription>} descriptionArray The array of
637  *     NavDescriptions to speak.
638  * @param {number} initialQueueMode The initial queue mode.
639  * @param {Function} completionFunction Function to call when finished speaking.
640  * @param {Object=} opt_personality Optional personality for all descriptions.
641  * @param {string=} opt_category Optional category for all descriptions.
642  */
643 cvox.NavigationManager.prototype.speakDescriptionArray = function(
644     descriptionArray,
645     initialQueueMode,
646     completionFunction,
647     opt_personality,
648     opt_category) {
649   if (opt_personality) {
650     descriptionArray.forEach(function(desc) {
651       if (!desc.personality) {
652         desc.personality = opt_personality;
653       }
654     });
655   }
656   if (opt_category) {
657     descriptionArray.forEach(function(desc) {
658       if (!desc.category) {
659         desc.category = opt_category;
660       }
661     });
662   }
663
664   this.navSpeaker_.speakDescriptionArray(
665       descriptionArray, initialQueueMode, completionFunction);
666 };
667
668 /**
669  * Add the position of the node on the page.
670  * @param {Node} node The node that ChromeVox should update the position.
671  */
672 cvox.NavigationManager.prototype.updatePosition = function(node) {
673   var msg = cvox.ChromeVox.position;
674   msg[document.location.href] =
675       cvox.DomUtil.elementToPoint(node);
676
677   cvox.ChromeVox.host.sendToBackgroundPage({
678     'target': 'Prefs',
679     'action': 'setPref',
680     'pref': 'position',
681     'value': JSON.stringify(msg)
682   });
683 };
684
685
686 // TODO(stoarca): The stuff below belongs in its own layer.
687 /**
688  * Perform all of the actions that should happen at the end of any
689  * navigation operation: update the lens, play earcons, and speak the
690  * description of the object that was navigated to.
691  *
692  * @param {string=} opt_prefix The string to be prepended to what
693  * is spoken to the user.
694  * @param {boolean=} opt_setFocus Whether or not to focus the current node.
695  * Defaults to true.
696  * @param {number=} opt_queueMode Initial queue mode to use.
697  * @param {function(): ?=} opt_callback Function to call after speaking.
698  */
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');
709       }
710       cvox.ChromeVox.tts.speak(msg, cvox.AbstractTts.QUEUE_MODE_QUEUE,
711           cvox.AbstractTts.PERSONALITY_ANNOTATION);
712     }
713     return;
714   }
715
716   if (this.enteredShifter_ || this.exitedShifter_) {
717     opt_prefix = cvox.ChromeVox.msgs.getMsg(
718         'enter_content_say', [this.shifter_.getName()]);
719   }
720
721   var descriptionArray = cvox.ChromeVox.navigationManager.getDescription();
722
723   opt_setFocus = opt_setFocus === undefined ? true : opt_setFocus;
724
725   if (opt_setFocus) {
726     this.setFocus();
727   }
728   this.updateIndicator();
729
730   var queueMode = opt_queueMode || cvox.AbstractTts.QUEUE_MODE_FLUSH;
731
732   if (opt_prefix) {
733     cvox.ChromeVox.tts.speak(
734         opt_prefix, queueMode, cvox.AbstractTts.PERSONALITY_ANNOTATION);
735     queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
736   }
737   this.speakDescriptionArray(descriptionArray,
738                              queueMode,
739                              opt_callback || null,
740                              null,
741                              'nav');
742
743   this.getBraille().write();
744
745   this.updatePosition(this.getCurrentNode());
746 };
747
748
749 /**
750  * Moves forward. Stops any subnavigation.
751  * @param {boolean=} opt_ignoreIframes Ignore iframes when navigating. Defaults
752  * to not ignore iframes.
753  * @param {number=} opt_granularity Optionally, switches to granularity before
754  * navigation.
755  * @return {boolean} False if end of document reached.
756  */
757 cvox.NavigationManager.prototype.navigate = function(
758     opt_ignoreIframes, opt_granularity) {
759   this.pageEndAnnounced_ = false;
760   if (this.pageEnd_) {
761     this.pageEnd_ = false;
762     this.syncToBeginning(opt_ignoreIframes);
763     return true;
764   }
765   if (!this.resolve()) {
766     return false;
767   }
768   this.ensureNotSubnavigating();
769   if (opt_granularity !== undefined &&
770       (opt_granularity !== this.getGranularity() ||
771           this.shifterStack_.length > 0)) {
772     this.setGranularity(opt_granularity, true);
773     this.sync();
774   }
775   return this.next_(!opt_ignoreIframes);
776 };
777
778
779 /**
780  * Moves forward after switching to a lower granularity until the next
781  * call to navigate().
782  */
783 cvox.NavigationManager.prototype.subnavigate = function() {
784   this.pageEndAnnounced_ = false;
785   if (!this.resolve()) {
786     return;
787   }
788   this.ensureSubnavigating();
789   this.next_(true);
790 };
791
792
793 /**
794  * Moves forward. Starts reading the page from that node.
795  * Uses QUEUE_MODE_FLUSH to flush any previous speech.
796  * @return {boolean} False if not "reading from here". True otherwise.
797  */
798 cvox.NavigationManager.prototype.skip = function() {
799   if (!this.keepReading_) {
800     return false;
801   }
802   if (cvox.ChromeVox.host.hasTtsCallback()) {
803     this.skipped_ = true;
804     this.setReversed(false);
805     this.startCallbackReading_(cvox.AbstractTts.QUEUE_MODE_FLUSH);
806   }
807   return true;
808 };
809
810
811 /**
812  * Starts reading the page from the current selection.
813  * @param {number} queueMode Either flush or queue.
814  */
815 cvox.NavigationManager.prototype.startReading = function(queueMode) {
816   this.keepReading_ = true;
817   if (cvox.ChromeVox.host.hasTtsCallback()) {
818     this.startCallbackReading_(queueMode);
819   } else {
820     this.startNonCallbackReading_(queueMode);
821   }
822   cvox.ChromeVox.stickyOverride = true;
823 };
824
825 /**
826  * Stops continuous read.
827  * @param {boolean} stopTtsImmediately True if the TTS should immediately stop
828  * speaking.
829  */
830 cvox.NavigationManager.prototype.stopReading = function(stopTtsImmediately) {
831   this.keepReading_ = false;
832   this.navSpeaker_.stopReading = true;
833   if (stopTtsImmediately) {
834     cvox.ChromeVox.tts.stop();
835   }
836   cvox.ChromeVox.stickyOverride = null;
837 };
838
839
840 /**
841  * The current current state of continuous read.
842  * @return {boolean} The state.
843  */
844 cvox.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  */
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);
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  */
870 cvox.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  */
894 cvox.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  */
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.
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  */
926 cvox.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  */
937 cvox.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  */
994 cvox.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  */
1003 cvox.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  */
1020 cvox.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  */
1030 cvox.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  */
1048 cvox.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  */
1070 cvox.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  */
1086 cvox.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  */
1095 cvox.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  */
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)) {
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  */
1136 cvox.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  */
1183 cvox.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  */
1198 cvox.NavigationManager.prototype.ignoreIframesNoMatterWhat = function() {
1199   this.ignoreIframesNoMatterWhat_ = true;
1200 };
1201
1202
1203 /**
1204  * Save a cursor selection during an excursion.
1205  */
1206 cvox.NavigationManager.prototype.saveSel = function() {
1207   this.saveSel_ = this.curSel_;
1208 };
1209
1210
1211 /**
1212  * Save a cursor selection after an excursion.
1213  */
1214 cvox.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  */
1224 cvox.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 };