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