Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / chromeos / chromevox / chromevox / injected / event_watcher.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 Watches for events in the browser such as focus changes.
7  *
8  */
9
10 goog.provide('cvox.ChromeVoxEventWatcher');
11 goog.provide('cvox.ChromeVoxEventWatcherUtil');
12
13 goog.require('cvox.ActiveIndicator');
14 goog.require('cvox.ApiImplementation');
15 goog.require('cvox.AriaUtil');
16 goog.require('cvox.ChromeVox');
17 goog.require('cvox.ChromeVoxEditableTextBase');
18 goog.require('cvox.ChromeVoxEventSuspender');
19 goog.require('cvox.ChromeVoxHTMLDateWidget');
20 goog.require('cvox.ChromeVoxHTMLMediaWidget');
21 goog.require('cvox.ChromeVoxHTMLTimeWidget');
22 goog.require('cvox.ChromeVoxKbHandler');
23 goog.require('cvox.ChromeVoxUserCommands');
24 goog.require('cvox.DomUtil');
25 goog.require('cvox.Focuser');
26 goog.require('cvox.History');
27 goog.require('cvox.LiveRegions');
28 goog.require('cvox.Memoize');
29 goog.require('cvox.NavigationSpeaker');
30 goog.require('cvox.PlatformFilter');  // TODO: Find a better place for this.
31 goog.require('cvox.PlatformUtil');
32 goog.require('cvox.QueueMode');
33 goog.require('cvox.TextHandlerInterface');
34 goog.require('cvox.UserEventDetail');
35
36 /**
37  * @constructor
38  */
39 cvox.ChromeVoxEventWatcher = function() {
40 };
41
42 /**
43  * The maximum amount of time to wait before processing events.
44  * A max time is needed so that even if a page is constantly updating,
45  * events will still go through.
46  * @const
47  * @type {number}
48  * @private
49  */
50 cvox.ChromeVoxEventWatcher.MAX_WAIT_TIME_MS_ = 50;
51
52 /**
53  * As long as the MAX_WAIT_TIME_ has not been exceeded, the event processor
54  * will wait this long after the last event was received before starting to
55  * process events.
56  * @const
57  * @type {number}
58  * @private
59  */
60 cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_ = 10;
61
62 /**
63  * Maximum number of live regions that we will attempt to process.
64  * @const
65  * @type {number}
66  * @private
67  */
68 cvox.ChromeVoxEventWatcher.MAX_LIVE_REGIONS_ = 5;
69
70
71 /**
72  * Whether or not ChromeVox should echo keys.
73  * It is useful to turn this off in case the system is already echoing keys (for
74  * example, in Android).
75  *
76  * @type {boolean}
77  */
78 cvox.ChromeVoxEventWatcher.shouldEchoKeys = true;
79
80
81 /**
82  * Whether or not the next utterance should flush all previous speech.
83  * Immediately after a key down or user action, we make the next speech
84  * flush, but otherwise it's better to do a category flush, so if a single
85  * user action generates both a focus change and a live region change,
86  * both get spoken.
87  * @type {boolean}
88  */
89 cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
90
91
92 /**
93  * Inits the event watcher and adds listeners.
94  * @param {!Document|!Window} doc The DOM document to add event listeners to.
95  */
96 cvox.ChromeVoxEventWatcher.init = function(doc) {
97   /**
98    * @type {Object}
99    */
100   cvox.ChromeVoxEventWatcher.lastFocusedNode = null;
101
102   /**
103    * @type {Object}
104    */
105   cvox.ChromeVoxEventWatcher.announcedMouseOverNode = null;
106
107   /**
108    * @type {Object}
109    */
110   cvox.ChromeVoxEventWatcher.pendingMouseOverNode = null;
111
112   /**
113    * @type {number?}
114    */
115   cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
116
117   /**
118    * @type {string?}
119    */
120   cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = null;
121
122   /**
123    * @type {Object}
124    */
125   cvox.ChromeVoxEventWatcher.eventToEat = null;
126
127   /**
128    * @type {Element}
129    */
130   cvox.ChromeVoxEventWatcher.currentTextControl = null;
131
132   /**
133    * @type {cvox.ChromeVoxEditableTextBase}
134    */
135   cvox.ChromeVoxEventWatcher.currentTextHandler = null;
136
137   /**
138    * Array of event listeners we've added so we can unregister them if needed.
139    * @type {Array}
140    * @private
141    */
142   cvox.ChromeVoxEventWatcher.listeners_ = [];
143
144   /**
145    * The mutation observer we use to listen for live regions.
146    * @type {MutationObserver}
147    * @private
148    */
149   cvox.ChromeVoxEventWatcher.mutationObserver_ = null;
150
151   /**
152    * Whether or not mouse hover events should trigger focusing.
153    * @type {boolean}
154    */
155   cvox.ChromeVoxEventWatcher.focusFollowsMouse = false;
156
157   /**
158    * The delay before a mouseover triggers focusing or announcing anything.
159    * @type {number}
160    */
161   cvox.ChromeVoxEventWatcher.mouseoverDelayMs = 500;
162
163   /**
164    * Array of events that need to be processed.
165    * @type {Array.<Event>}
166    * @private
167    */
168   cvox.ChromeVoxEventWatcher.events_ = new Array();
169
170   /**
171    * The time when the last event was received.
172    * @type {number}
173    */
174   cvox.ChromeVoxEventWatcher.lastEventTime = 0;
175
176   /**
177    * The timestamp for the first unprocessed event.
178    * @type {number}
179    */
180   cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime = -1;
181
182   /**
183    * Whether or not queue processing is scheduled to run.
184    * @type {boolean}
185    * @private
186    */
187   cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = false;
188
189   /**
190    * A list of callbacks to be called when the EventWatcher has
191    * completed processing all events in its queue.
192    * @type {Array.<function()>}
193    * @private
194    */
195   cvox.ChromeVoxEventWatcher.readyCallbacks_ = new Array();
196
197
198 /**
199  * tracks whether we've received two or more key up's while pass through mode
200  * is active.
201  * @type {boolean}
202  * @private
203  */
204 cvox.ChromeVoxEventWatcher.secondPassThroughKeyUp_ = false;
205
206   /**
207    * Whether or not the ChromeOS Search key (keyCode == 91) is being held.
208    *
209    * We must track this manually because on ChromeOS, the Search key being held
210    * down does not cause keyEvent.metaKey to be set.
211    *
212    * TODO (clchen, dmazzoni): Refactor this since there are edge cases
213    * where manually tracking key down and key up can fail (such as when
214    * the user switches tabs before letting go of the key being held).
215    *
216    * @type {boolean}
217    */
218   cvox.ChromeVox.searchKeyHeld = false;
219
220   /**
221    * The mutation observer that listens for chagnes to text controls
222    * that might not send other events.
223    * @type {MutationObserver}
224    * @private
225    */
226   cvox.ChromeVoxEventWatcher.textMutationObserver_ = null;
227
228   cvox.ChromeVoxEventWatcher.addEventListeners_(doc);
229 };
230
231
232 /**
233  * Stores state variables in a provided object.
234  *
235  * @param {Object} store The object.
236  */
237 cvox.ChromeVoxEventWatcher.storeOn = function(store) {
238   store['searchKeyHeld'] = cvox.ChromeVox.searchKeyHeld;
239 };
240
241 /**
242  * Updates the object with state variables from an earlier storeOn call.
243  *
244  * @param {Object} store The object.
245  */
246 cvox.ChromeVoxEventWatcher.readFrom = function(store) {
247   cvox.ChromeVox.searchKeyHeld = store['searchKeyHeld'];
248 };
249
250 /**
251  * Adds an event to the events queue and updates the time when the last
252  * event was received.
253  *
254  * @param {Event} evt The event to be added to the events queue.
255  * @param {boolean=} opt_ignoreVisibility Whether to ignore visibility
256  * checking on the document. By default, this is set to false (so an
257  * invisible document would result in this event not being added).
258  */
259 cvox.ChromeVoxEventWatcher.addEvent = function(evt, opt_ignoreVisibility) {
260   // Don't add any events to the events queue if ChromeVox is inactive or the
261   // page is hidden unless specified to not do so.
262   if (!cvox.ChromeVox.isActive ||
263       (document.webkitHidden && !opt_ignoreVisibility)) {
264     return;
265   }
266   cvox.ChromeVoxEventWatcher.events_.push(evt);
267   cvox.ChromeVoxEventWatcher.lastEventTime = new Date().getTime();
268   if (cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime == -1) {
269     cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime = new Date().getTime();
270   }
271   if (!cvox.ChromeVoxEventWatcher.queueProcessingScheduled_) {
272     cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = true;
273     window.setTimeout(cvox.ChromeVoxEventWatcher.processQueue_,
274         cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_);
275   }
276 };
277
278 /**
279  * Adds a callback to be called when the event watcher has finished
280  * processing all pending events.
281  * @param {Function} cb The callback.
282  */
283 cvox.ChromeVoxEventWatcher.addReadyCallback = function(cb) {
284   cvox.ChromeVoxEventWatcher.readyCallbacks_.push(cb);
285   cvox.ChromeVoxEventWatcher.maybeCallReadyCallbacks_();
286 };
287
288 /**
289  * Returns whether or not there are pending events.
290  * @return {boolean} Whether or not there are pending events.
291  * @private
292  */
293 cvox.ChromeVoxEventWatcher.hasPendingEvents_ = function() {
294   return cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime != -1 ||
295       cvox.ChromeVoxEventWatcher.queueProcessingScheduled_;
296 };
297
298
299 /**
300  * A bit used to make sure only one ready callback is pending at a time.
301  * @private
302  */
303 cvox.ChromeVoxEventWatcher.readyCallbackRunning_ = false;
304
305 /**
306  * Checks if the event watcher has pending events.  If not, call the oldest
307  * readyCallback in a loop until exhausted or until there are pending events.
308  * @private
309  */
310 cvox.ChromeVoxEventWatcher.maybeCallReadyCallbacks_ = function() {
311   if (!cvox.ChromeVoxEventWatcher.readyCallbackRunning_) {
312     cvox.ChromeVoxEventWatcher.readyCallbackRunning_ = true;
313     window.setTimeout(function() {
314       cvox.ChromeVoxEventWatcher.readyCallbackRunning_ = false;
315       if (!cvox.ChromeVoxEventWatcher.hasPendingEvents_() &&
316              !cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ &&
317              cvox.ChromeVoxEventWatcher.readyCallbacks_.length > 0) {
318         cvox.ChromeVoxEventWatcher.readyCallbacks_.shift()();
319         cvox.ChromeVoxEventWatcher.maybeCallReadyCallbacks_();
320       }
321     }, 5);
322   }
323 };
324
325
326 /**
327  * Add all of our event listeners to the document.
328  * @param {!Document|!Window} doc The DOM document to add event listeners to.
329  * @private
330  */
331 cvox.ChromeVoxEventWatcher.addEventListeners_ = function(doc) {
332   // We always need key down listeners to intercept activate/deactivate.
333   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
334       'keydown', cvox.ChromeVoxEventWatcher.keyDownEventWatcher, true);
335
336   // If ChromeVox isn't active, skip all other event listeners.
337   if (!cvox.ChromeVox.isActive || cvox.ChromeVox.entireDocumentIsHidden) {
338     return;
339   }
340   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
341       'keypress', cvox.ChromeVoxEventWatcher.keyPressEventWatcher, true);
342   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
343       'keyup', cvox.ChromeVoxEventWatcher.keyUpEventWatcher, true);
344   // Listen for our own events to handle public user commands if the web app
345   // doesn't do it for us.
346   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
347       cvox.UserEventDetail.Category.JUMP,
348       cvox.ChromeVoxUserCommands.handleChromeVoxUserEvent,
349       false);
350
351   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
352       'focus', cvox.ChromeVoxEventWatcher.focusEventWatcher, true);
353   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
354       'blur', cvox.ChromeVoxEventWatcher.blurEventWatcher, true);
355   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
356       'change', cvox.ChromeVoxEventWatcher.changeEventWatcher, true);
357   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
358       'copy', cvox.ChromeVoxEventWatcher.clipboardEventWatcher, true);
359   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
360       'cut', cvox.ChromeVoxEventWatcher.clipboardEventWatcher, true);
361   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
362       'paste', cvox.ChromeVoxEventWatcher.clipboardEventWatcher, true);
363   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
364       'select', cvox.ChromeVoxEventWatcher.selectEventWatcher, true);
365
366   // TODO(dtseng): Experimental, see:
367   // https://developers.google.com/chrome/whitepapers/pagevisibility
368   cvox.ChromeVoxEventWatcher.addEventListener_(doc, 'webkitvisibilitychange',
369       cvox.ChromeVoxEventWatcher.visibilityChangeWatcher, true);
370   cvox.ChromeVoxEventWatcher.events_ = new Array();
371   cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = false;
372
373   // Handle mouse events directly without going into the events queue.
374   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
375       'mouseover', cvox.ChromeVoxEventWatcher.mouseOverEventWatcher, true);
376   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
377       'mouseout', cvox.ChromeVoxEventWatcher.mouseOutEventWatcher, true);
378
379   // With the exception of non-Android, click events go through the event queue.
380   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
381       'click', cvox.ChromeVoxEventWatcher.mouseClickEventWatcher, true);
382
383   cvox.ChromeVoxEventWatcher.mutationObserver_ =
384       new window.WebKitMutationObserver(
385           cvox.ChromeVoxEventWatcher.mutationHandler);
386   var observerTarget = null;
387   if (doc.documentElement) {
388     observerTarget = doc.documentElement;
389   } else if (doc.document && doc.document.documentElement) {
390     observerTarget = doc.document.documentElement;
391   }
392   if (observerTarget) {
393     cvox.ChromeVoxEventWatcher.mutationObserver_.observe(
394         observerTarget,
395         /** @type {!MutationObserverInit} */ ({
396           childList: true,
397           attributes: true,
398           characterData: true,
399           subtree: true,
400           attributeOldValue: true,
401           characterDataOldValue: true
402         }));
403   }
404 };
405
406
407 /**
408  * Remove all registered event watchers.
409  * @param {!Document|!Window} doc The DOM document to add event listeners to.
410  */
411 cvox.ChromeVoxEventWatcher.cleanup = function(doc) {
412   for (var i = 0; i < cvox.ChromeVoxEventWatcher.listeners_.length; i++) {
413     var listener = cvox.ChromeVoxEventWatcher.listeners_[i];
414     doc.removeEventListener(
415         listener.type, listener.listener, listener.useCapture);
416   }
417   cvox.ChromeVoxEventWatcher.listeners_ = [];
418   if (cvox.ChromeVoxEventWatcher.currentDateHandler) {
419     cvox.ChromeVoxEventWatcher.currentDateHandler.shutdown();
420   }
421   if (cvox.ChromeVoxEventWatcher.currentTimeHandler) {
422     cvox.ChromeVoxEventWatcher.currentTimeHandler.shutdown();
423   }
424   if (cvox.ChromeVoxEventWatcher.currentMediaHandler) {
425     cvox.ChromeVoxEventWatcher.currentMediaHandler.shutdown();
426   }
427   if (cvox.ChromeVoxEventWatcher.mutationObserver_) {
428     cvox.ChromeVoxEventWatcher.mutationObserver_.disconnect();
429   }
430   cvox.ChromeVoxEventWatcher.mutationObserver_ = null;
431 };
432
433 /**
434  * Add one event listener and save the data so it can be removed later.
435  * @param {!Document|!Window} doc The DOM document to add event listeners to.
436  * @param {string} type The event type.
437  * @param {EventListener|function(Event):(boolean|undefined)} listener
438  *     The function to be called when the event is fired.
439  * @param {boolean} useCapture Whether this listener should capture events
440  *     before they're sent to targets beneath it in the DOM tree.
441  * @private
442  */
443 cvox.ChromeVoxEventWatcher.addEventListener_ = function(doc, type,
444     listener, useCapture) {
445   cvox.ChromeVoxEventWatcher.listeners_.push(
446       {'type': type, 'listener': listener, 'useCapture': useCapture});
447   doc.addEventListener(type, listener, useCapture);
448 };
449
450 /**
451  * Return the last focused node.
452  * @return {Object} The last node that was focused.
453  */
454 cvox.ChromeVoxEventWatcher.getLastFocusedNode = function() {
455   return cvox.ChromeVoxEventWatcher.lastFocusedNode;
456 };
457
458 /**
459  * Sets the last focused node.
460  * @param {Element} element The last focused element.
461  *
462  * @private
463  */
464 cvox.ChromeVoxEventWatcher.setLastFocusedNode_ = function(element) {
465   cvox.ChromeVoxEventWatcher.lastFocusedNode = element;
466   cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = !element ? null :
467       cvox.DomUtil.getControlValueAndStateString(element);
468 };
469
470 /**
471  * Called when there's any mutation of the document. We use this to
472  * handle live region updates.
473  * @param {Array.<MutationRecord>} mutations The mutations.
474  * @return {boolean} True if the default action should be performed.
475  */
476 cvox.ChromeVoxEventWatcher.mutationHandler = function(mutations) {
477   if (cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
478     return true;
479   }
480
481   cvox.ChromeVox.navigationManager.updateIndicatorIfChanged();
482
483   cvox.LiveRegions.processMutations(
484       mutations,
485       function(assertive, navDescriptions) {
486         var evt = new window.Event('LiveRegion');
487         evt.navDescriptions = navDescriptions;
488         evt.assertive = assertive;
489         cvox.ChromeVoxEventWatcher.addEvent(evt, true);
490         return true;
491       });
492 };
493
494
495 /**
496  * Handles mouseclick events.
497  * Mouseclick events are only triggered if the user touches the mouse;
498  * we use it to determine whether or not we should bother trying to sync to a
499  * selection.
500  * @param {Event} evt The mouseclick event to process.
501  * @return {boolean} True if the default action should be performed.
502  */
503 cvox.ChromeVoxEventWatcher.mouseClickEventWatcher = function(evt) {
504   if (evt.fromCvox) {
505     return true;
506   }
507
508   if (cvox.ChromeVox.host.mustRedispatchClickEvent()) {
509     cvox.ChromeVoxUserCommands.wasMouseClicked = true;
510     evt.stopPropagation();
511     evt.preventDefault();
512     // Since the click event was caught and we are re-dispatching it, we also
513     // need to refocus the current node because the current node has already
514     // been blurred by the window getting the click event in the first place.
515     // Failing to restore focus before clicking can cause odd problems such as
516     // the soft IME not coming up in Android (it only shows up if the click
517     // happens in a focused text field).
518     cvox.Focuser.setFocus(cvox.ChromeVox.navigationManager.getCurrentNode());
519     cvox.ChromeVox.tts.speak(
520         cvox.ChromeVox.msgs.getMsg('element_clicked'),
521         cvox.ChromeVoxEventWatcher.queueMode_(),
522         cvox.AbstractTts.PERSONALITY_ANNOTATION);
523     var targetNode = cvox.ChromeVox.navigationManager.getCurrentNode();
524     // If the targetNode has a defined onclick function, just call it directly
525     // rather than try to generate a click event and dispatching it.
526     // While both work equally well on standalone Chrome, when dealing with
527     // embedded WebViews, generating a click event and sending it is not always
528     // reliable since the framework may swallow the event.
529     cvox.DomUtil.clickElem(targetNode, false, true);
530     return false;
531   } else {
532     cvox.ChromeVoxEventWatcher.addEvent(evt);
533   }
534   cvox.ChromeVoxUserCommands.wasMouseClicked = true;
535   return true;
536 };
537
538 /**
539  * Handles mouseover events.
540  * Mouseover events are only triggered if the user touches the mouse, so
541  * for users who only use the keyboard, this will have no effect.
542  *
543  * @param {Event} evt The mouseover event to process.
544  * @return {boolean} True if the default action should be performed.
545  */
546 cvox.ChromeVoxEventWatcher.mouseOverEventWatcher = function(evt) {
547   // Chrome simulates the meta key for mouse events generated from
548   // touch exploration.
549   var isTouchEvent = (evt.metaKey);
550
551   var mouseoverDelayMs = cvox.ChromeVoxEventWatcher.mouseoverDelayMs;
552   if (isTouchEvent) {
553     mouseoverDelayMs = 0;
554   } else if (!cvox.ChromeVoxEventWatcher.focusFollowsMouse) {
555     return true;
556   }
557
558   if (cvox.DomUtil.isDescendantOfNode(
559       cvox.ChromeVoxEventWatcher.announcedMouseOverNode, evt.target)) {
560     return true;
561   }
562
563   if (evt.target == cvox.ChromeVoxEventWatcher.pendingMouseOverNode) {
564     return true;
565   }
566
567   cvox.ChromeVoxEventWatcher.pendingMouseOverNode = evt.target;
568   if (cvox.ChromeVoxEventWatcher.mouseOverTimeoutId) {
569     window.clearTimeout(cvox.ChromeVoxEventWatcher.mouseOverTimeoutId);
570     cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
571   }
572
573   if (evt.target.tagName && (evt.target.tagName == 'BODY')) {
574     cvox.ChromeVoxEventWatcher.pendingMouseOverNode = null;
575     cvox.ChromeVoxEventWatcher.announcedMouseOverNode = null;
576     return true;
577   }
578
579   // Only focus and announce if the mouse stays over the same target
580   // for longer than the given delay.
581   cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = window.setTimeout(
582       function() {
583         cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
584         if (evt.target != cvox.ChromeVoxEventWatcher.pendingMouseOverNode) {
585           return;
586         }
587
588         cvox.Memoize.scope(function() {
589           cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = true;
590           cvox.ChromeVox.navigationManager.stopReading(true);
591           var target = /** @type {Node} */(evt.target);
592           cvox.Focuser.setFocus(target);
593           cvox.ApiImplementation.syncToNode(
594               target, true, cvox.ChromeVoxEventWatcher.queueMode_());
595           cvox.ChromeVoxEventWatcher.announcedMouseOverNode = target;
596         });
597       }, mouseoverDelayMs);
598
599   return true;
600 };
601
602 /**
603  * Handles mouseout events.
604  *
605  * @param {Event} evt The mouseout event to process.
606  * @return {boolean} True if the default action should be performed.
607  */
608 cvox.ChromeVoxEventWatcher.mouseOutEventWatcher = function(evt) {
609   if (evt.target == cvox.ChromeVoxEventWatcher.pendingMouseOverNode) {
610     cvox.ChromeVoxEventWatcher.pendingMouseOverNode = null;
611     if (cvox.ChromeVoxEventWatcher.mouseOverTimeoutId) {
612       window.clearTimeout(cvox.ChromeVoxEventWatcher.mouseOverTimeoutId);
613       cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
614     }
615   }
616
617   return true;
618 };
619
620
621 /**
622  * Watches for focus events.
623  *
624  * @param {Event} evt The focus event to add to the queue.
625  * @return {boolean} True if the default action should be performed.
626  */
627 cvox.ChromeVoxEventWatcher.focusEventWatcher = function(evt) {
628   // First remove any dummy spans. We create dummy spans in UserCommands in
629   // order to sync the browser's default tab action with the user's current
630   // navigation position.
631   cvox.ChromeVoxUserCommands.removeTabDummySpan();
632
633   if (!cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
634     cvox.ChromeVoxEventWatcher.addEvent(evt);
635   } else if (evt.target && evt.target.nodeType == Node.ELEMENT_NODE) {
636     cvox.ChromeVoxEventWatcher.setLastFocusedNode_(
637         /** @type {Element} */(evt.target));
638   }
639   return true;
640 };
641
642 /**
643  * Handles for focus events passed to it from the events queue.
644  *
645  * @param {Event} evt The focus event to handle.
646  */
647 cvox.ChromeVoxEventWatcher.focusHandler = function(evt) {
648   if (evt.target &&
649       evt.target.hasAttribute &&
650       evt.target.getAttribute('aria-hidden') == 'true' &&
651       evt.target.getAttribute('chromevoxignoreariahidden') != 'true') {
652     cvox.ChromeVoxEventWatcher.setLastFocusedNode_(null);
653     cvox.ChromeVoxEventWatcher.setUpTextHandler();
654     return;
655   }
656   if (evt.target && evt.target != window) {
657     var target = /** @type {Element} */(evt.target);
658     var parentControl = cvox.DomUtil.getSurroundingControl(target);
659     if (parentControl &&
660         parentControl == cvox.ChromeVoxEventWatcher.lastFocusedNode) {
661       cvox.ChromeVoxEventWatcher.handleControlChanged(target);
662       return;
663     }
664
665     if (parentControl) {
666       cvox.ChromeVoxEventWatcher.setLastFocusedNode_(
667           /** @type {Element} */(parentControl));
668     } else {
669       cvox.ChromeVoxEventWatcher.setLastFocusedNode_(target);
670     }
671
672     var queueMode = cvox.ChromeVoxEventWatcher.queueMode_();
673
674     if (cvox.ChromeVoxEventWatcher.getInitialVisibility() ||
675         cvox.ChromeVoxEventWatcher.handleDialogFocus(target)) {
676       queueMode = cvox.QueueMode.QUEUE;
677     }
678
679     if (cvox.ChromeVox.navigationManager.clearPageSel(true)) {
680       queueMode = cvox.QueueMode.QUEUE;
681     }
682
683     // Navigate to this control so that it will be the same for focus as for
684     // regular navigation.
685     cvox.ApiImplementation.syncToNode(
686         target, !document.webkitHidden, queueMode);
687
688     if ((evt.target.constructor == HTMLVideoElement) ||
689         (evt.target.constructor == HTMLAudioElement)) {
690       cvox.ChromeVoxEventWatcher.setUpMediaHandler_();
691       return;
692     }
693     if (evt.target.hasAttribute) {
694       var inputType = evt.target.getAttribute('type');
695       switch (inputType) {
696         case 'time':
697           cvox.ChromeVoxEventWatcher.setUpTimeHandler_();
698           return;
699         case 'date':
700         case 'month':
701         case 'week':
702           cvox.ChromeVoxEventWatcher.setUpDateHandler_();
703           return;
704       }
705     }
706     cvox.ChromeVoxEventWatcher.setUpTextHandler();
707   } else {
708     cvox.ChromeVoxEventWatcher.setLastFocusedNode_(null);
709   }
710   return;
711 };
712
713 /**
714  * Watches for blur events.
715  *
716  * @param {Event} evt The blur event to add to the queue.
717  * @return {boolean} True if the default action should be performed.
718  */
719 cvox.ChromeVoxEventWatcher.blurEventWatcher = function(evt) {
720   window.setTimeout(function() {
721     if (!document.activeElement) {
722       cvox.ChromeVoxEventWatcher.setLastFocusedNode_(null);
723       cvox.ChromeVoxEventWatcher.addEvent(evt);
724     }
725   }, 0);
726   return true;
727 };
728
729 /**
730  * Watches for key down events.
731  *
732  * @param {Event} evt The keydown event to add to the queue.
733  * @return {boolean} True if the default action should be performed.
734  */
735 cvox.ChromeVoxEventWatcher.keyDownEventWatcher = function(evt) {
736   return /** @type {boolean} */ (cvox.Memoize.scope(
737       cvox.ChromeVoxEventWatcher.doKeyDownEventWatcher_.bind(this, evt)));
738 };
739
740 /**
741  * Implementation of |keyDownEventWatcher|.
742  *
743  * @param {Event} evt The keydown event to add to the queue.
744  * @return {boolean} True if the default action should be performed.
745  * @private
746  */
747 cvox.ChromeVoxEventWatcher.doKeyDownEventWatcher_ = function(evt) {
748   cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = true;
749
750   if (cvox.ChromeVox.passThroughMode) {
751     return true;
752   }
753
754   if (cvox.ChromeVox.isChromeOS && evt.keyCode == 91) {
755     cvox.ChromeVox.searchKeyHeld = true;
756   }
757
758   // Store some extra ChromeVox-specific properties in the event.
759   evt.searchKeyHeld =
760       cvox.ChromeVox.searchKeyHeld && cvox.ChromeVox.isActive;
761   evt.stickyMode = cvox.ChromeVox.isStickyModeOn() && cvox.ChromeVox.isActive;
762   evt.keyPrefix = cvox.ChromeVox.keyPrefixOn && cvox.ChromeVox.isActive;
763
764   cvox.ChromeVox.keyPrefixOn = false;
765
766   cvox.ChromeVoxEventWatcher.eventToEat = null;
767   if (!cvox.ChromeVoxKbHandler.basicKeyDownActionsListener(evt) ||
768       cvox.ChromeVoxEventWatcher.handleControlAction(evt)) {
769     // Swallow the event immediately to prevent the arrow keys
770     // from driving controls on the web page.
771     evt.preventDefault();
772     evt.stopPropagation();
773     // Also mark this as something to be swallowed when the followup
774     // keypress/keyup counterparts to this event show up later.
775     cvox.ChromeVoxEventWatcher.eventToEat = evt;
776     return false;
777   }
778   cvox.ChromeVoxEventWatcher.addEvent(evt);
779   return true;
780 };
781
782 /**
783  * Watches for key up events.
784  *
785  * @param {Event} evt The event to add to the queue.
786  * @return {boolean} True if the default action should be performed.
787  * @this {cvox.ChromeVoxEventWatcher}
788  */
789 cvox.ChromeVoxEventWatcher.keyUpEventWatcher = function(evt) {
790   if (evt.keyCode == 91) {
791     cvox.ChromeVox.searchKeyHeld = false;
792   }
793
794   if (cvox.ChromeVox.passThroughMode) {
795     if (!evt.ctrlKey && !evt.altKey && !evt.metaKey && !evt.shiftKey &&
796         !cvox.ChromeVox.searchKeyHeld) {
797       // Only reset pass through on the second key up without modifiers since
798       // the first one is from the pass through shortcut itself.
799       if (this.secondPassThroughKeyUp_) {
800         this.secondPassThroughKeyUp_ = false;
801         cvox.ChromeVox.passThroughMode = false;
802       } else {
803         this.secondPassThroughKeyUp_ = true;
804       }
805     }
806     return true;
807   }
808
809   if (cvox.ChromeVoxEventWatcher.eventToEat &&
810       evt.keyCode == cvox.ChromeVoxEventWatcher.eventToEat.keyCode) {
811     evt.stopPropagation();
812     evt.preventDefault();
813     return false;
814   }
815
816   cvox.ChromeVoxEventWatcher.addEvent(evt);
817
818   return true;
819 };
820
821 /**
822  * Watches for key press events.
823  *
824  * @param {Event} evt The event to add to the queue.
825  * @return {boolean} True if the default action should be performed.
826  */
827 cvox.ChromeVoxEventWatcher.keyPressEventWatcher = function(evt) {
828   var url = document.location.href;
829   // Use ChromeVox.typingEcho as default value.
830   var speakChar = cvox.TypingEcho.shouldSpeakChar(cvox.ChromeVox.typingEcho);
831
832   if (typeof cvox.ChromeVox.keyEcho[url] !== 'undefined') {
833     speakChar = cvox.ChromeVox.keyEcho[url];
834   }
835
836   // Directly handle typed characters here while key echo is on. This
837   // skips potentially costly computations (especially for content editable).
838   // This is done deliberately for the sake of responsiveness and in some cases
839   // (e.g. content editable), to have characters echoed properly.
840   if (cvox.ChromeVoxEditableTextBase.eventTypingEcho && (speakChar &&
841           cvox.DomPredicates.editTextPredicate([document.activeElement])) &&
842       document.activeElement.type !== 'password') {
843     cvox.ChromeVox.tts.speak(String.fromCharCode(evt.charCode),
844                              cvox.QueueMode.FLUSH);
845   }
846   cvox.ChromeVoxEventWatcher.addEvent(evt);
847   if (cvox.ChromeVoxEventWatcher.eventToEat &&
848       evt.keyCode == cvox.ChromeVoxEventWatcher.eventToEat.keyCode) {
849     evt.preventDefault();
850     evt.stopPropagation();
851     return false;
852   }
853   return true;
854 };
855
856 /**
857  * Watches for change events.
858  *
859  * @param {Event} evt The event to add to the queue.
860  * @return {boolean} True if the default action should be performed.
861  */
862 cvox.ChromeVoxEventWatcher.changeEventWatcher = function(evt) {
863   cvox.ChromeVoxEventWatcher.addEvent(evt);
864   return true;
865 };
866
867 // TODO(dtseng): ChromeVoxEditableText interrupts cut and paste announcements.
868 /**
869  * Watches for cut, copy, and paste events.
870  *
871  * @param {Event} evt The event to process.
872  * @return {boolean} True if the default action should be performed.
873  */
874 cvox.ChromeVoxEventWatcher.clipboardEventWatcher = function(evt) {
875   cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg(evt.type).toLowerCase(),
876                            cvox.QueueMode.QUEUE);
877   var text = '';
878   switch (evt.type) {
879   case 'paste':
880     text = evt.clipboardData.getData('text');
881     break;
882   case 'copy':
883   case 'cut':
884     text = window.getSelection().toString();
885     break;
886   }
887   cvox.ChromeVox.tts.speak(text, cvox.QueueMode.QUEUE);
888   cvox.ChromeVox.navigationManager.clearPageSel();
889   return true;
890 };
891
892 /**
893  * Handles change events passed to it from the events queue.
894  *
895  * @param {Event} evt The event to handle.
896  */
897 cvox.ChromeVoxEventWatcher.changeHandler = function(evt) {
898   if (cvox.ChromeVoxEventWatcher.setUpTextHandler()) {
899     return;
900   }
901   if (document.activeElement == evt.target) {
902     cvox.ChromeVoxEventWatcher.handleControlChanged(document.activeElement);
903   }
904 };
905
906 /**
907  * Watches for select events.
908  *
909  * @param {Event} evt The event to add to the queue.
910  * @return {boolean} True if the default action should be performed.
911  */
912 cvox.ChromeVoxEventWatcher.selectEventWatcher = function(evt) {
913   cvox.ChromeVoxEventWatcher.addEvent(evt);
914   return true;
915 };
916
917 /**
918  * Listens for WebKit visibility change events.
919  */
920 cvox.ChromeVoxEventWatcher.visibilityChangeWatcher = function() {
921   cvox.ChromeVoxEventWatcher.initialVisibility = !document.webkitHidden;
922   if (document.webkitHidden) {
923     cvox.ChromeVox.navigationManager.stopReading(true);
924   }
925 };
926
927 /**
928  * Gets the initial visibility of the page.
929  * @return {boolean} True if the page is visible and this is the first request
930  * for visibility state.
931  */
932 cvox.ChromeVoxEventWatcher.getInitialVisibility = function() {
933   var ret = cvox.ChromeVoxEventWatcher.initialVisibility;
934   cvox.ChromeVoxEventWatcher.initialVisibility = false;
935   return ret;
936 };
937
938 /**
939  * Speaks the text of one live region.
940  * @param {boolean} assertive True if it's an assertive live region.
941  * @param {Array.<cvox.NavDescription>} messages An array of navDescriptions
942  *    representing the description of the live region changes.
943  * @private
944  */
945 cvox.ChromeVoxEventWatcher.speakLiveRegion_ = function(
946     assertive, messages) {
947   var queueMode = cvox.ChromeVoxEventWatcher.queueMode_();
948   var descSpeaker = new cvox.NavigationSpeaker();
949   descSpeaker.speakDescriptionArray(messages, queueMode, null);
950 };
951
952 /**
953  * Sets up the text handler.
954  * @return {boolean} True if an editable text control has focus.
955  */
956 cvox.ChromeVoxEventWatcher.setUpTextHandler = function() {
957   var currentFocus = document.activeElement;
958   if (currentFocus &&
959       currentFocus.hasAttribute &&
960       currentFocus.getAttribute('aria-hidden') == 'true' &&
961       currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
962     currentFocus = null;
963   }
964
965   if (currentFocus != cvox.ChromeVoxEventWatcher.currentTextControl) {
966     if (cvox.ChromeVoxEventWatcher.currentTextControl) {
967       cvox.ChromeVoxEventWatcher.currentTextControl.removeEventListener(
968           'input', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
969       cvox.ChromeVoxEventWatcher.currentTextControl.removeEventListener(
970           'click', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
971       if (cvox.ChromeVoxEventWatcher.textMutationObserver_) {
972         cvox.ChromeVoxEventWatcher.textMutationObserver_.disconnect();
973         cvox.ChromeVoxEventWatcher.textMutationObserver_ = null;
974       }
975     }
976     cvox.ChromeVoxEventWatcher.currentTextControl = null;
977     if (cvox.ChromeVoxEventWatcher.currentTextHandler) {
978       cvox.ChromeVoxEventWatcher.currentTextHandler.teardown();
979       cvox.ChromeVoxEventWatcher.currentTextHandler = null;
980     }
981     if (currentFocus == null) {
982       return false;
983     }
984     if (currentFocus.constructor == HTMLInputElement &&
985         cvox.DomUtil.isInputTypeText(currentFocus) &&
986         cvox.ChromeVoxEventWatcher.shouldEchoKeys) {
987       cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus;
988       cvox.ChromeVoxEventWatcher.currentTextHandler =
989           new cvox.ChromeVoxEditableHTMLInput(currentFocus, cvox.ChromeVox.tts);
990     } else if ((currentFocus.constructor == HTMLTextAreaElement) &&
991         cvox.ChromeVoxEventWatcher.shouldEchoKeys) {
992       cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus;
993       cvox.ChromeVoxEventWatcher.currentTextHandler =
994           new cvox.ChromeVoxEditableTextArea(currentFocus, cvox.ChromeVox.tts);
995     } else if (currentFocus.isContentEditable ||
996                currentFocus.getAttribute('role') == 'textbox') {
997       cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus;
998       cvox.ChromeVoxEventWatcher.currentTextHandler =
999           new cvox.ChromeVoxEditableContentEditable(currentFocus,
1000               cvox.ChromeVox.tts);
1001     }
1002
1003     if (cvox.ChromeVoxEventWatcher.currentTextControl) {
1004       cvox.ChromeVoxEventWatcher.currentTextControl.addEventListener(
1005           'input', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
1006       cvox.ChromeVoxEventWatcher.currentTextControl.addEventListener(
1007           'click', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
1008       if (window.WebKitMutationObserver) {
1009         cvox.ChromeVoxEventWatcher.textMutationObserver_ =
1010             new window.WebKitMutationObserver(
1011                 cvox.ChromeVoxEventWatcher.onTextMutation);
1012         cvox.ChromeVoxEventWatcher.textMutationObserver_.observe(
1013             cvox.ChromeVoxEventWatcher.currentTextControl,
1014             /** @type {!MutationObserverInit} */ ({
1015               childList: true,
1016               attributes: true,
1017               subtree: true,
1018               attributeOldValue: false,
1019               characterDataOldValue: false
1020             }));
1021       }
1022       if (!cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
1023         cvox.ChromeVox.navigationManager.updateSel(
1024             cvox.CursorSelection.fromNode(
1025                 cvox.ChromeVoxEventWatcher.currentTextControl));
1026       }
1027     }
1028
1029     return (null != cvox.ChromeVoxEventWatcher.currentTextHandler);
1030   }
1031 };
1032
1033 /**
1034  * Speaks updates to editable text controls as needed.
1035  *
1036  * @param {boolean} isKeypress Was this change triggered by a keypress?
1037  * @return {boolean} True if an editable text control has focus.
1038  */
1039 cvox.ChromeVoxEventWatcher.handleTextChanged = function(isKeypress) {
1040   if (cvox.ChromeVoxEventWatcher.currentTextHandler) {
1041     var handler = cvox.ChromeVoxEventWatcher.currentTextHandler;
1042     var shouldFlush = false;
1043     if (isKeypress && cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance) {
1044       shouldFlush = true;
1045       cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
1046     }
1047     handler.update(shouldFlush);
1048     cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
1049     return true;
1050   }
1051   return false;
1052 };
1053
1054 /**
1055  * Called when an editable text control has focus, because many changes
1056  * to a text box don't ever generate events - e.g. if the page's javascript
1057  * changes the contents of the text box after some delay, or if it's
1058  * contentEditable or a generic div with role="textbox".
1059  */
1060 cvox.ChromeVoxEventWatcher.onTextMutation = function() {
1061   if (cvox.ChromeVoxEventWatcher.currentTextHandler) {
1062     window.setTimeout(function() {
1063       cvox.ChromeVoxEventWatcher.handleTextChanged(false);
1064     }, cvox.ChromeVoxEventWatcher.MAX_WAIT_TIME_MS_);
1065   }
1066 };
1067
1068 /**
1069  * Speaks updates to other form controls as needed.
1070  * @param {Element} control The target control.
1071  */
1072 cvox.ChromeVoxEventWatcher.handleControlChanged = function(control) {
1073   var newValue = cvox.DomUtil.getControlValueAndStateString(control);
1074   var parentControl = cvox.DomUtil.getSurroundingControl(control);
1075   var announceChange = false;
1076
1077   if (control != cvox.ChromeVoxEventWatcher.lastFocusedNode &&
1078       (parentControl == null ||
1079        parentControl != cvox.ChromeVoxEventWatcher.lastFocusedNode)) {
1080     cvox.ChromeVoxEventWatcher.setLastFocusedNode_(control);
1081   } else if (newValue == cvox.ChromeVoxEventWatcher.lastFocusedNodeValue) {
1082     return;
1083   }
1084
1085   cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = newValue;
1086   if (cvox.DomPredicates.checkboxPredicate([control]) ||
1087       cvox.DomPredicates.radioPredicate([control])) {
1088     // Always announce changes to checkboxes and radio buttons.
1089     announceChange = true;
1090     // Play earcons for checkboxes and radio buttons
1091     if (control.checked) {
1092       cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.CHECK_ON);
1093     } else {
1094       cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.CHECK_OFF);
1095     }
1096   }
1097
1098   if (control.tagName == 'SELECT') {
1099     announceChange = true;
1100   }
1101
1102   if (control.tagName == 'INPUT') {
1103     switch (control.type) {
1104       case 'color':
1105       case 'datetime':
1106       case 'datetime-local':
1107       case 'range':
1108         announceChange = true;
1109         break;
1110       default:
1111         break;
1112     }
1113   }
1114
1115   // Always announce changes for anything with an ARIA role.
1116   if (control.hasAttribute && control.hasAttribute('role')) {
1117     announceChange = true;
1118   }
1119
1120   if ((parentControl &&
1121       parentControl != control &&
1122       document.activeElement == control)) {
1123     // If focus has been set on a child of the parent control, we need to
1124     // sync to that node so that ChromeVox navigation will be in sync with
1125     // focus navigation.
1126     cvox.ApiImplementation.syncToNode(
1127         control, true,
1128         cvox.ChromeVoxEventWatcher.queueMode_());
1129     announceChange = false;
1130   } else if (cvox.AriaUtil.getActiveDescendant(control)) {
1131     cvox.ChromeVox.navigationManager.updateSelToArbitraryNode(
1132         cvox.AriaUtil.getActiveDescendant(control),
1133         true);
1134
1135     announceChange = true;
1136   }
1137
1138   if (announceChange && !cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
1139     cvox.ChromeVox.tts.speak(newValue,
1140                              cvox.ChromeVoxEventWatcher.queueMode_(),
1141                              null);
1142     cvox.NavBraille.fromText(newValue).write();
1143   }
1144 };
1145
1146 /**
1147  * Handle actions on form controls triggered by key presses.
1148  * @param {Object} evt The event.
1149  * @return {boolean} True if this key event was handled.
1150  */
1151 cvox.ChromeVoxEventWatcher.handleControlAction = function(evt) {
1152   // Ignore the control action if ChromeVox is not active.
1153   if (!cvox.ChromeVox.isActive) {
1154     return false;
1155   }
1156   var control = evt.target;
1157
1158   if (control.tagName == 'SELECT' && (control.size <= 1) &&
1159       (evt.keyCode == 13 || evt.keyCode == 32)) { // Enter or Space
1160     // TODO (dmazzoni, clchen): Remove this workaround once accessibility
1161     // APIs make browser based popups accessible.
1162     //
1163     // Do nothing, but eat this keystroke when the SELECT control
1164     // has a dropdown style since if we don't, it will generate
1165     // a browser popup menu which is not accessible.
1166     // List style SELECT controls are fine and don't need this workaround.
1167     evt.preventDefault();
1168     evt.stopPropagation();
1169     return true;
1170   }
1171
1172   if (control.tagName == 'INPUT' && control.type == 'range') {
1173     var value = parseFloat(control.value);
1174     var step;
1175     if (control.step && control.step > 0.0) {
1176       step = control.step;
1177     } else if (control.min && control.max) {
1178       var range = (control.max - control.min);
1179       if (range > 2 && range < 31) {
1180         step = 1;
1181       } else {
1182         step = (control.max - control.min) / 10;
1183       }
1184     } else {
1185       step = 1;
1186     }
1187
1188     if (evt.keyCode == 37 || evt.keyCode == 38) {  // left or up
1189       value -= step;
1190     } else if (evt.keyCode == 39 || evt.keyCode == 40) {  // right or down
1191       value += step;
1192     }
1193
1194     if (control.max && value > control.max) {
1195       value = control.max;
1196     }
1197     if (control.min && value < control.min) {
1198       value = control.min;
1199     }
1200
1201     control.value = value;
1202   }
1203   return false;
1204 };
1205
1206 /**
1207  * When an element receives focus, see if we've entered or left a dialog
1208  * and return a string describing the event.
1209  *
1210  * @param {Element} target The element that just received focus.
1211  * @return {boolean} True if an announcement was spoken.
1212  */
1213 cvox.ChromeVoxEventWatcher.handleDialogFocus = function(target) {
1214   var dialog = target;
1215   var role = '';
1216   while (dialog) {
1217     if (dialog.hasAttribute) {
1218       role = dialog.getAttribute('role');
1219       if (role == 'dialog' || role == 'alertdialog') {
1220         break;
1221       }
1222     }
1223     dialog = dialog.parentElement;
1224   }
1225
1226   if (dialog == cvox.ChromeVox.navigationManager.currentDialog) {
1227     return false;
1228   }
1229
1230   if (cvox.ChromeVox.navigationManager.currentDialog && !dialog) {
1231     if (!cvox.DomUtil.isDescendantOfNode(
1232         document.activeElement,
1233         cvox.ChromeVox.navigationManager.currentDialog)) {
1234       cvox.ChromeVox.navigationManager.currentDialog = null;
1235
1236       cvox.ChromeVoxEventWatcher.speakAnnotationWithCategory_(
1237           cvox.ChromeVox.msgs.getMsg('exiting_dialog'),
1238           cvox.TtsCategory.NAV);
1239       return true;
1240     }
1241   } else {
1242     if (dialog) {
1243       cvox.ChromeVox.navigationManager.currentDialog = dialog;
1244       cvox.ChromeVoxEventWatcher.speakAnnotationWithCategory_(
1245           cvox.ChromeVox.msgs.getMsg('entering_dialog'),
1246           cvox.TtsCategory.NAV);
1247
1248       if (role == 'alertdialog') {
1249         var dialogDescArray =
1250             cvox.DescriptionUtil.getFullDescriptionsFromChildren(null, dialog);
1251         var descSpeaker = new cvox.NavigationSpeaker();
1252         descSpeaker.speakDescriptionArray(dialogDescArray,
1253                                           cvox.QueueMode.QUEUE,
1254                                           null);
1255       }
1256       return true;
1257     }
1258   }
1259   return false;
1260 };
1261
1262 /**
1263  * Speak the given text with the annotation personality and the given
1264  * speech queue utterance category.
1265  * @param {string} text The text to speak.
1266  * @param {string} category The category of text, used by the speech queue
1267  *     when flushing all speech from the same category while leaving other
1268  *     speech in the queue.
1269  * @private
1270  */
1271 cvox.ChromeVoxEventWatcher.speakAnnotationWithCategory_ = function(
1272     text, category) {
1273   var properties = {};
1274   var src = cvox.AbstractTts.PERSONALITY_ANNOTATION;
1275   for (var key in src) {
1276     properties[key] = src[key];
1277   }
1278   properties['category'] = category;
1279   cvox.ChromeVox.tts.speak(
1280       text,
1281       cvox.ChromeVoxEventWatcher.queueMode_(),
1282       properties);
1283 };
1284
1285 /**
1286  * Returns true if we should wait to process events.
1287  * @param {number} lastFocusTimestamp The timestamp of the last focus event.
1288  * @param {number} firstTimestamp The timestamp of the first event.
1289  * @param {number} currentTime The current timestamp.
1290  * @return {boolean} True if we should wait to process events.
1291  */
1292 cvox.ChromeVoxEventWatcherUtil.shouldWaitToProcess = function(
1293     lastFocusTimestamp, firstTimestamp, currentTime) {
1294   var timeSinceFocusEvent = currentTime - lastFocusTimestamp;
1295   var timeSinceFirstEvent = currentTime - firstTimestamp;
1296   return timeSinceFocusEvent < cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_ &&
1297       timeSinceFirstEvent < cvox.ChromeVoxEventWatcher.MAX_WAIT_TIME_MS_;
1298 };
1299
1300
1301 /**
1302  * Returns the queue mode to use for the next utterance spoken as
1303  * a result of an event or navigation. The first utterance that's spoken
1304  * after an explicit user action like a key press will flush, and
1305  * subsequent events will return a category flush.
1306  * @return {cvox.QueueMode} The queue mode.
1307  * @private
1308  */
1309 cvox.ChromeVoxEventWatcher.queueMode_ = function() {
1310   if (cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance) {
1311     cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
1312     return cvox.QueueMode.FLUSH;
1313   }
1314   return cvox.QueueMode.CATEGORY_FLUSH;
1315 };
1316
1317
1318 /**
1319  * Processes the events queue.
1320  *
1321  * @private
1322  */
1323 cvox.ChromeVoxEventWatcher.processQueue_ = function() {
1324   cvox.Memoize.scope(cvox.ChromeVoxEventWatcher.doProcessQueue_);
1325 };
1326
1327 /**
1328  * Implementation of |processQueue_|.
1329  *
1330  * @private
1331  */
1332 cvox.ChromeVoxEventWatcher.doProcessQueue_ = function() {
1333   // Return now if there are no events in the queue.
1334   if (cvox.ChromeVoxEventWatcher.events_.length == 0) {
1335     return;
1336   }
1337
1338   // Look for the most recent focus event and delete any preceding event
1339   // that applied to whatever was focused previously.
1340   var events = cvox.ChromeVoxEventWatcher.events_;
1341   var lastFocusIndex = -1;
1342   var lastFocusTimestamp = 0;
1343   var evt;
1344   var i;
1345   for (i = 0; evt = events[i]; i++) {
1346     if (evt.type == 'focus') {
1347       lastFocusIndex = i;
1348       lastFocusTimestamp = evt.timeStamp;
1349     }
1350   }
1351   cvox.ChromeVoxEventWatcher.events_ = [];
1352   for (i = 0; evt = events[i]; i++) {
1353     var prevEvt = events[i - 1] || {};
1354     if ((i >= lastFocusIndex || evt.type == 'LiveRegion') &&
1355         (prevEvt.type != 'focus' || evt.type != 'change')) {
1356       cvox.ChromeVoxEventWatcher.events_.push(evt);
1357     }
1358   }
1359
1360   cvox.ChromeVoxEventWatcher.events_.sort(function(a, b) {
1361     if (b.type != 'LiveRegion' && a.type == 'LiveRegion') {
1362       return 1;
1363     }
1364     return -1;
1365   });
1366
1367   // If the most recent focus event was very recent, wait for things to
1368   // settle down before processing events, unless the max wait time has
1369   // passed.
1370   var currentTime = new Date().getTime();
1371   if (lastFocusIndex >= 0 &&
1372       cvox.ChromeVoxEventWatcherUtil.shouldWaitToProcess(
1373           lastFocusTimestamp,
1374           cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime,
1375           currentTime)) {
1376     window.setTimeout(cvox.ChromeVoxEventWatcher.processQueue_,
1377                       cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_);
1378     return;
1379   }
1380
1381   // Process the remaining events in the queue, in order.
1382   for (i = 0; evt = cvox.ChromeVoxEventWatcher.events_[i]; i++) {
1383     cvox.ChromeVoxEventWatcher.handleEvent_(evt);
1384   }
1385   cvox.ChromeVoxEventWatcher.events_ = new Array();
1386   cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime = -1;
1387   cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = false;
1388   cvox.ChromeVoxEventWatcher.maybeCallReadyCallbacks_();
1389 };
1390
1391 /**
1392  * Handle events from the queue by routing them to their respective handlers.
1393  *
1394  * @private
1395  * @param {Event} evt The event to be handled.
1396  */
1397 cvox.ChromeVoxEventWatcher.handleEvent_ = function(evt) {
1398   switch (evt.type) {
1399     case 'keydown':
1400     case 'input':
1401       cvox.ChromeVoxEventWatcher.setUpTextHandler();
1402       if (cvox.ChromeVoxEventWatcher.currentTextControl) {
1403         cvox.ChromeVoxEventWatcher.handleTextChanged(true);
1404
1405         var editableText = /** @type {cvox.ChromeVoxEditableTextBase} */
1406             (cvox.ChromeVoxEventWatcher.currentTextHandler);
1407         if (editableText && editableText.lastChangeDescribed) {
1408           break;
1409         }
1410       }
1411       // We're either not on a text control, or we are on a text control but no
1412       // text change was described. Let's try describing the state instead.
1413       cvox.ChromeVoxEventWatcher.handleControlChanged(document.activeElement);
1414       break;
1415     case 'keyup':
1416       // Some controls change only after key up.
1417       cvox.ChromeVoxEventWatcher.handleControlChanged(document.activeElement);
1418       break;
1419     case 'keypress':
1420       cvox.ChromeVoxEventWatcher.setUpTextHandler();
1421       break;
1422     case 'click':
1423       cvox.ApiImplementation.syncToNode(/** @type {Node} */(evt.target), true);
1424       break;
1425     case 'focus':
1426       cvox.ChromeVoxEventWatcher.focusHandler(evt);
1427       break;
1428     case 'blur':
1429       cvox.ChromeVoxEventWatcher.setUpTextHandler();
1430       break;
1431     case 'change':
1432       cvox.ChromeVoxEventWatcher.changeHandler(evt);
1433       break;
1434     case 'select':
1435       cvox.ChromeVoxEventWatcher.setUpTextHandler();
1436       break;
1437     case 'LiveRegion':
1438       cvox.ChromeVoxEventWatcher.speakLiveRegion_(
1439           evt.assertive, evt.navDescriptions);
1440       break;
1441   }
1442 };
1443
1444
1445 /**
1446  * Sets up the time handler.
1447  * @return {boolean} True if a time control has focus.
1448  * @private
1449  */
1450 cvox.ChromeVoxEventWatcher.setUpTimeHandler_ = function() {
1451   var currentFocus = document.activeElement;
1452   if (currentFocus &&
1453       currentFocus.hasAttribute &&
1454       currentFocus.getAttribute('aria-hidden') == 'true' &&
1455       currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1456     currentFocus = null;
1457   }
1458   if (currentFocus.constructor == HTMLInputElement &&
1459       currentFocus.type && (currentFocus.type == 'time')) {
1460     cvox.ChromeVoxEventWatcher.currentTimeHandler =
1461         new cvox.ChromeVoxHTMLTimeWidget(currentFocus, cvox.ChromeVox.tts);
1462     } else {
1463       cvox.ChromeVoxEventWatcher.currentTimeHandler = null;
1464     }
1465   return (null != cvox.ChromeVoxEventWatcher.currentTimeHandler);
1466 };
1467
1468
1469 /**
1470  * Sets up the media (video/audio) handler.
1471  * @return {boolean} True if a media control has focus.
1472  * @private
1473  */
1474 cvox.ChromeVoxEventWatcher.setUpMediaHandler_ = function() {
1475   var currentFocus = document.activeElement;
1476   if (currentFocus &&
1477       currentFocus.hasAttribute &&
1478       currentFocus.getAttribute('aria-hidden') == 'true' &&
1479       currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1480     currentFocus = null;
1481   }
1482   if ((currentFocus.constructor == HTMLVideoElement) ||
1483       (currentFocus.constructor == HTMLAudioElement)) {
1484     cvox.ChromeVoxEventWatcher.currentMediaHandler =
1485         new cvox.ChromeVoxHTMLMediaWidget(currentFocus, cvox.ChromeVox.tts);
1486     } else {
1487       cvox.ChromeVoxEventWatcher.currentMediaHandler = null;
1488     }
1489   return (null != cvox.ChromeVoxEventWatcher.currentMediaHandler);
1490 };
1491
1492 /**
1493  * Sets up the date handler.
1494  * @return {boolean} True if a date control has focus.
1495  * @private
1496  */
1497 cvox.ChromeVoxEventWatcher.setUpDateHandler_ = function() {
1498   var currentFocus = document.activeElement;
1499   if (currentFocus &&
1500       currentFocus.hasAttribute &&
1501       currentFocus.getAttribute('aria-hidden') == 'true' &&
1502       currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1503     currentFocus = null;
1504   }
1505   if (currentFocus.constructor == HTMLInputElement &&
1506       currentFocus.type &&
1507       ((currentFocus.type == 'date') ||
1508       (currentFocus.type == 'month') ||
1509       (currentFocus.type == 'week'))) {
1510     cvox.ChromeVoxEventWatcher.currentDateHandler =
1511         new cvox.ChromeVoxHTMLDateWidget(currentFocus, cvox.ChromeVox.tts);
1512     } else {
1513       cvox.ChromeVoxEventWatcher.currentDateHandler = null;
1514     }
1515   return (null != cvox.ChromeVoxEventWatcher.currentDateHandler);
1516 };