6aa21cf2deb9a3872f95d72c6a6f4da314cf100c
[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   var hasTouch = 'ontouchstart' in window;
586   var mouseoverDelayMs = cvox.ChromeVoxEventWatcher.mouseoverDelayMs;
587   if (hasTouch) {
588     mouseoverDelayMs = 0;
589   } else if (!cvox.ChromeVoxEventWatcher.focusFollowsMouse) {
590     return true;
591   }
592
593   if (cvox.DomUtil.isDescendantOfNode(
594       cvox.ChromeVoxEventWatcher.announcedMouseOverNode, evt.target)) {
595     return true;
596   }
597
598   if (evt.target == cvox.ChromeVoxEventWatcher.pendingMouseOverNode) {
599     return true;
600   }
601
602   cvox.ChromeVoxEventWatcher.pendingMouseOverNode = evt.target;
603   if (cvox.ChromeVoxEventWatcher.mouseOverTimeoutId) {
604     window.clearTimeout(cvox.ChromeVoxEventWatcher.mouseOverTimeoutId);
605     cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
606   }
607
608   if (evt.target.tagName && (evt.target.tagName == 'BODY')) {
609     cvox.ChromeVoxEventWatcher.pendingMouseOverNode = null;
610     cvox.ChromeVoxEventWatcher.announcedMouseOverNode = null;
611     return true;
612   }
613
614   // Only focus and announce if the mouse stays over the same target
615   // for longer than the given delay.
616   cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = window.setTimeout(
617       function() {
618         cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
619         if (evt.target != cvox.ChromeVoxEventWatcher.pendingMouseOverNode) {
620           return;
621         }
622         cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = true;
623         cvox.ChromeVox.navigationManager.stopReading(true);
624         var target = /** @type {Node} */(evt.target);
625         cvox.Focuser.setFocus(target);
626         cvox.ApiImplementation.syncToNode(
627             target, true, cvox.ChromeVoxEventWatcher.queueMode_());
628         cvox.ChromeVoxEventWatcher.announcedMouseOverNode = target;
629       }, mouseoverDelayMs);
630
631   return true;
632 };
633
634 /**
635  * Handles mouseout events.
636  *
637  * @param {Event} evt The mouseout event to process.
638  * @return {boolean} True if the default action should be performed.
639  */
640 cvox.ChromeVoxEventWatcher.mouseOutEventWatcher = function(evt) {
641   if (evt.target == cvox.ChromeVoxEventWatcher.pendingMouseOverNode) {
642     cvox.ChromeVoxEventWatcher.pendingMouseOverNode = null;
643     if (cvox.ChromeVoxEventWatcher.mouseOverTimeoutId) {
644       window.clearTimeout(cvox.ChromeVoxEventWatcher.mouseOverTimeoutId);
645       cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
646     }
647   }
648
649   return true;
650 };
651
652
653 /**
654  * Watches for focus events.
655  *
656  * @param {Event} evt The focus event to add to the queue.
657  * @return {boolean} True if the default action should be performed.
658  */
659 cvox.ChromeVoxEventWatcher.focusEventWatcher = function(evt) {
660   // First remove any dummy spans. We create dummy spans in UserCommands in
661   // order to sync the browser's default tab action with the user's current
662   // navigation position.
663   cvox.ChromeVoxUserCommands.removeTabDummySpan();
664
665   if (!cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
666     cvox.ChromeVoxEventWatcher.addEvent(evt);
667   } else if (evt.target && evt.target.nodeType == Node.ELEMENT_NODE) {
668     cvox.ChromeVoxEventWatcher.setLastFocusedNode_(
669         /** @type {Element} */(evt.target));
670   }
671   return true;
672 };
673
674 /**
675  * Handles for focus events passed to it from the events queue.
676  *
677  * @param {Event} evt The focus event to handle.
678  */
679 cvox.ChromeVoxEventWatcher.focusHandler = function(evt) {
680   if (evt.target &&
681       evt.target.hasAttribute &&
682       evt.target.getAttribute('aria-hidden') == 'true' &&
683       evt.target.getAttribute('chromevoxignoreariahidden') != 'true') {
684     cvox.ChromeVoxEventWatcher.setLastFocusedNode_(null);
685     cvox.ChromeVoxEventWatcher.setUpTextHandler();
686     return;
687   }
688   if (evt.target && evt.target != window) {
689     var target = /** @type {Element} */(evt.target);
690     var parentControl = cvox.DomUtil.getSurroundingControl(target);
691     if (parentControl &&
692         parentControl == cvox.ChromeVoxEventWatcher.lastFocusedNode) {
693       cvox.ChromeVoxEventWatcher.handleControlChanged(target);
694       return;
695     }
696
697     if (parentControl) {
698       cvox.ChromeVoxEventWatcher.setLastFocusedNode_(
699           /** @type {Element} */(parentControl));
700     } else {
701       cvox.ChromeVoxEventWatcher.setLastFocusedNode_(target);
702     }
703
704     var queueMode = cvox.ChromeVoxEventWatcher.queueMode_();
705
706     if (cvox.ChromeVoxEventWatcher.getInitialVisibility() ||
707         cvox.ChromeVoxEventWatcher.handleDialogFocus(target)) {
708       queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
709     }
710
711     if (cvox.ChromeVox.navigationManager.clearPageSel(true)) {
712       queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
713     }
714
715     // Navigate to this control so that it will be the same for focus as for
716     // regular navigation.
717     cvox.ApiImplementation.syncToNode(
718         target, !document.webkitHidden, queueMode);
719
720     if ((evt.target.constructor == HTMLVideoElement) ||
721         (evt.target.constructor == HTMLAudioElement)) {
722       cvox.ChromeVoxEventWatcher.setUpMediaHandler_();
723       return;
724     }
725     if (evt.target.hasAttribute) {
726       var inputType = evt.target.getAttribute('type');
727       switch (inputType) {
728         case 'time':
729           cvox.ChromeVoxEventWatcher.setUpTimeHandler_();
730           return;
731         case 'date':
732         case 'month':
733         case 'week':
734           cvox.ChromeVoxEventWatcher.setUpDateHandler_();
735           return;
736       }
737     }
738     cvox.ChromeVoxEventWatcher.setUpTextHandler();
739   } else {
740     cvox.ChromeVoxEventWatcher.setLastFocusedNode_(null);
741   }
742   return;
743 };
744
745 /**
746  * Watches for blur events.
747  *
748  * @param {Event} evt The blur event to add to the queue.
749  * @return {boolean} True if the default action should be performed.
750  */
751 cvox.ChromeVoxEventWatcher.blurEventWatcher = function(evt) {
752   window.setTimeout(function() {
753     if (!document.activeElement) {
754       cvox.ChromeVoxEventWatcher.setLastFocusedNode_(null);
755       cvox.ChromeVoxEventWatcher.addEvent(evt);
756     }
757   }, 0);
758   return true;
759 };
760
761 /**
762  * Watches for key down events.
763  *
764  * @param {Event} evt The keydown event to add to the queue.
765  * @return {boolean} True if the default action should be performed.
766  */
767 cvox.ChromeVoxEventWatcher.keyDownEventWatcher = function(evt) {
768   cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = true;
769
770   if (cvox.ChromeVox.passThroughMode) {
771     return true;
772   }
773
774   if (cvox.ChromeVox.isChromeOS && evt.keyCode == 91) {
775     cvox.ChromeVox.searchKeyHeld = true;
776   }
777
778   // Store some extra ChromeVox-specific properties in the event.
779   evt.searchKeyHeld =
780       cvox.ChromeVox.searchKeyHeld && cvox.ChromeVox.isActive;
781   evt.stickyMode = cvox.ChromeVox.isStickyModeOn() && cvox.ChromeVox.isActive;
782   evt.keyPrefix = cvox.ChromeVox.keyPrefixOn && cvox.ChromeVox.isActive;
783
784   cvox.ChromeVox.keyPrefixOn = false;
785
786   cvox.ChromeVoxEventWatcher.eventToEat = null;
787   if (!cvox.ChromeVoxKbHandler.basicKeyDownActionsListener(evt) ||
788       cvox.ChromeVoxEventWatcher.handleControlAction(evt)) {
789     // Swallow the event immediately to prevent the arrow keys
790     // from driving controls on the web page.
791     evt.preventDefault();
792     evt.stopPropagation();
793     // Also mark this as something to be swallowed when the followup
794     // keypress/keyup counterparts to this event show up later.
795     cvox.ChromeVoxEventWatcher.eventToEat = evt;
796     return false;
797   }
798   cvox.ChromeVoxEventWatcher.addEvent(evt);
799   return true;
800 };
801
802 /**
803  * Watches for key up events.
804  *
805  * @param {Event} evt The event to add to the queue.
806  * @return {boolean} True if the default action should be performed.
807  * @this {cvox.ChromeVoxEventWatcher}
808  */
809 cvox.ChromeVoxEventWatcher.keyUpEventWatcher = function(evt) {
810   if (evt.keyCode == 91) {
811     cvox.ChromeVox.searchKeyHeld = false;
812   }
813
814   if (cvox.ChromeVox.passThroughMode) {
815     if (!evt.ctrlKey && !evt.altKey && !evt.metaKey && !evt.shiftKey &&
816         !cvox.ChromeVox.searchKeyHeld) {
817       // Only reset pass through on the second key up without modifiers since
818       // the first one is from the pass through shortcut itself.
819       if (this.secondPassThroughKeyUp_) {
820         this.secondPassThroughKeyUp_ = false;
821         cvox.ChromeVox.passThroughMode = false;
822       } else {
823         this.secondPassThroughKeyUp_ = true;
824       }
825     }
826     return true;
827   }
828
829   if (cvox.ChromeVoxEventWatcher.eventToEat &&
830       evt.keyCode == cvox.ChromeVoxEventWatcher.eventToEat.keyCode) {
831     evt.stopPropagation();
832     evt.preventDefault();
833     return false;
834   }
835
836   cvox.ChromeVoxEventWatcher.addEvent(evt);
837
838   return true;
839 };
840
841 /**
842  * Watches for key press events.
843  *
844  * @param {Event} evt The event to add to the queue.
845  * @return {boolean} True if the default action should be performed.
846  */
847 cvox.ChromeVoxEventWatcher.keyPressEventWatcher = function(evt) {
848   var url = document.location.href;
849   // Use ChromeVox.typingEcho as default value.
850   var speakChar = cvox.TypingEcho.shouldSpeakChar(cvox.ChromeVox.typingEcho);
851
852   if (typeof cvox.ChromeVox.keyEcho[url] !== 'undefined') {
853     speakChar = cvox.ChromeVox.keyEcho[url];
854   }
855
856   // Directly handle typed characters here while key echo is on. This
857   // skips potentially costly computations (especially for content editable).
858   // This is done deliberately for the sake of responsiveness and in some cases
859   // (e.g. content editable), to have characters echoed properly.
860   if (cvox.ChromeVoxEditableTextBase.eventTypingEcho && (speakChar &&
861           cvox.DomPredicates.editTextPredicate([document.activeElement])) &&
862       document.activeElement.type !== 'password') {
863     cvox.ChromeVox.tts.speak(String.fromCharCode(evt.charCode), 0);
864   }
865   cvox.ChromeVoxEventWatcher.addEvent(evt);
866   if (cvox.ChromeVoxEventWatcher.eventToEat &&
867       evt.keyCode == cvox.ChromeVoxEventWatcher.eventToEat.keyCode) {
868     evt.preventDefault();
869     evt.stopPropagation();
870     return false;
871   }
872   return true;
873 };
874
875 /**
876  * Watches for change events.
877  *
878  * @param {Event} evt The event to add to the queue.
879  * @return {boolean} True if the default action should be performed.
880  */
881 cvox.ChromeVoxEventWatcher.changeEventWatcher = function(evt) {
882   cvox.ChromeVoxEventWatcher.addEvent(evt);
883   return true;
884 };
885
886 // TODO(dtseng): ChromeVoxEditableText interrupts cut and paste announcements.
887 /**
888  * Watches for cut, copy, and paste events.
889  *
890  * @param {Event} evt The event to process.
891  * @return {boolean} True if the default action should be performed.
892  */
893 cvox.ChromeVoxEventWatcher.clipboardEventWatcher = function(evt) {
894   cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg(evt.type).toLowerCase());
895   var text = '';
896   switch (evt.type) {
897   case 'paste':
898     text = evt.clipboardData.getData('text');
899     break;
900   case 'copy':
901   case 'cut':
902     text = window.getSelection().toString();
903     break;
904   }
905   cvox.ChromeVox.tts.speak(text, cvox.AbstractTts.QUEUE_MODE_QUEUE);
906   cvox.ChromeVox.navigationManager.clearPageSel();
907   return true;
908 };
909
910 /**
911  * Handles change events passed to it from the events queue.
912  *
913  * @param {Event} evt The event to handle.
914  */
915 cvox.ChromeVoxEventWatcher.changeHandler = function(evt) {
916   if (cvox.ChromeVoxEventWatcher.setUpTextHandler()) {
917     return;
918   }
919   if (document.activeElement == evt.target) {
920     cvox.ChromeVoxEventWatcher.handleControlChanged(document.activeElement);
921   }
922 };
923
924 /**
925  * Watches for select events.
926  *
927  * @param {Event} evt The event to add to the queue.
928  * @return {boolean} True if the default action should be performed.
929  */
930 cvox.ChromeVoxEventWatcher.selectEventWatcher = function(evt) {
931   cvox.ChromeVoxEventWatcher.addEvent(evt);
932   return true;
933 };
934
935 /**
936  * Watches for DOM subtree modified events.
937  *
938  * @param {Event} evt The event to add to the queue.
939  * @return {boolean} True if the default action should be performed.
940  */
941 cvox.ChromeVoxEventWatcher.subtreeModifiedEventWatcher = function(evt) {
942   if (!evt || !evt.target) {
943     return true;
944   }
945   cvox.ChromeVoxEventWatcher.addEvent(evt);
946   return true;
947 };
948
949 /**
950  * Listens for WebKit visibility change events.
951  */
952 cvox.ChromeVoxEventWatcher.visibilityChangeWatcher = function() {
953   cvox.ChromeVoxEventWatcher.initialVisibility = !document.webkitHidden;
954   if (document.webkitHidden) {
955     cvox.ChromeVox.navigationManager.stopReading(true);
956   }
957 };
958
959 /**
960  * Gets the initial visibility of the page.
961  * @return {boolean} True if the page is visible and this is the first request
962  * for visibility state.
963  */
964 cvox.ChromeVoxEventWatcher.getInitialVisibility = function() {
965   var ret = cvox.ChromeVoxEventWatcher.initialVisibility;
966   cvox.ChromeVoxEventWatcher.initialVisibility = false;
967   return ret;
968 };
969
970 /**
971  * Speaks the text of one live region.
972  * @param {boolean} assertive True if it's an assertive live region.
973  * @param {Array.<cvox.NavDescription>} messages An array of navDescriptions
974  *    representing the description of the live region changes.
975  * @private
976  */
977 cvox.ChromeVoxEventWatcher.speakLiveRegion_ = function(
978     assertive, messages) {
979   var queueMode = cvox.ChromeVoxEventWatcher.queueMode_();
980   var descSpeaker = new cvox.NavigationSpeaker();
981   descSpeaker.speakDescriptionArray(messages, queueMode, null);
982 };
983
984 /**
985  * Handles DOM subtree modified events passed to it from the events queue.
986  * If the change involves an ARIA live region, then speak it.
987  *
988  * @param {Event} evt The event to handle.
989  */
990 cvox.ChromeVoxEventWatcher.subtreeModifiedHandler = function(evt) {
991   // Subtree modified events can happen in bursts. If several events happen at
992   // the same time, trying to process all of them will slow ChromeVox to
993   // a crawl and make the page itself unresponsive (ie, Google+).
994   // Before processing subtree modified events, make sure that it is not part of
995   // a large burst of events.
996   // TODO (clchen): Revisit this after the DOM mutation events are
997   // available in Chrome.
998   var currentTime = new Date().getTime();
999
1000   if ((cvox.ChromeVoxEventWatcher.lastSubtreeModifiedEventBurstTime_ +
1001       cvox.ChromeVoxEventWatcher.SUBTREE_MODIFIED_BURST_DURATION_) >
1002       currentTime) {
1003     cvox.ChromeVoxEventWatcher.subtreeModifiedEventsCount_++;
1004     if (cvox.ChromeVoxEventWatcher.subtreeModifiedEventsCount_ >
1005         cvox.ChromeVoxEventWatcher.SUBTREE_MODIFIED_BURST_COUNT_LIMIT_) {
1006       return;
1007     }
1008   } else {
1009     cvox.ChromeVoxEventWatcher.lastSubtreeModifiedEventBurstTime_ = currentTime;
1010     cvox.ChromeVoxEventWatcher.subtreeModifiedEventsCount_ = 1;
1011   }
1012
1013   if (!evt || !evt.target) {
1014     return;
1015   }
1016   var target = /** @type {Element} */ (evt.target);
1017   var regions = cvox.AriaUtil.getLiveRegions(target);
1018   for (var i = 0; (i < regions.length) &&
1019       (i < cvox.ChromeVoxEventWatcher.MAX_LIVE_REGIONS_); i++) {
1020     cvox.LiveRegionsDeprecated.updateLiveRegion(
1021         regions[i], cvox.ChromeVoxEventWatcher.queueMode_(), false);
1022   }
1023 };
1024
1025 /**
1026  * Sets up the text handler.
1027  * @return {boolean} True if an editable text control has focus.
1028  */
1029 cvox.ChromeVoxEventWatcher.setUpTextHandler = function() {
1030   var currentFocus = document.activeElement;
1031   if (currentFocus &&
1032       currentFocus.hasAttribute &&
1033       currentFocus.getAttribute('aria-hidden') == 'true' &&
1034       currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1035     currentFocus = null;
1036   }
1037
1038   if (currentFocus != cvox.ChromeVoxEventWatcher.currentTextControl) {
1039     if (cvox.ChromeVoxEventWatcher.currentTextControl) {
1040       cvox.ChromeVoxEventWatcher.currentTextControl.removeEventListener(
1041           'input', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
1042       cvox.ChromeVoxEventWatcher.currentTextControl.removeEventListener(
1043           'click', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
1044       if (cvox.ChromeVoxEventWatcher.textMutationObserver_) {
1045         cvox.ChromeVoxEventWatcher.textMutationObserver_.disconnect();
1046         cvox.ChromeVoxEventWatcher.textMutationObserver_ = null;
1047       }
1048     }
1049     cvox.ChromeVoxEventWatcher.currentTextControl = null;
1050     if (cvox.ChromeVoxEventWatcher.currentTextHandler) {
1051       cvox.ChromeVoxEventWatcher.currentTextHandler.teardown();
1052       cvox.ChromeVoxEventWatcher.currentTextHandler = null;
1053     }
1054     if (currentFocus == null) {
1055       return false;
1056     }
1057     if (currentFocus.constructor == HTMLInputElement &&
1058         cvox.DomUtil.isInputTypeText(currentFocus) &&
1059         cvox.ChromeVoxEventWatcher.shouldEchoKeys) {
1060       cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus;
1061       cvox.ChromeVoxEventWatcher.currentTextHandler =
1062           new cvox.ChromeVoxEditableHTMLInput(currentFocus, cvox.ChromeVox.tts);
1063     } else if ((currentFocus.constructor == HTMLTextAreaElement) &&
1064         cvox.ChromeVoxEventWatcher.shouldEchoKeys) {
1065       cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus;
1066       cvox.ChromeVoxEventWatcher.currentTextHandler =
1067           new cvox.ChromeVoxEditableTextArea(currentFocus, cvox.ChromeVox.tts);
1068     } else if (currentFocus.isContentEditable ||
1069                currentFocus.getAttribute('role') == 'textbox') {
1070       cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus;
1071       cvox.ChromeVoxEventWatcher.currentTextHandler =
1072           new cvox.ChromeVoxEditableContentEditable(currentFocus,
1073               cvox.ChromeVox.tts);
1074     }
1075
1076     if (cvox.ChromeVoxEventWatcher.currentTextControl) {
1077       cvox.ChromeVoxEventWatcher.currentTextControl.addEventListener(
1078           'input', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
1079       cvox.ChromeVoxEventWatcher.currentTextControl.addEventListener(
1080           'click', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
1081       if (window.WebKitMutationObserver) {
1082         cvox.ChromeVoxEventWatcher.textMutationObserver_ =
1083             new window.WebKitMutationObserver(
1084                 cvox.ChromeVoxEventWatcher.onTextMutation);
1085         cvox.ChromeVoxEventWatcher.textMutationObserver_.observe(
1086             cvox.ChromeVoxEventWatcher.currentTextControl,
1087             /** @type {!MutationObserverInit} */ ({
1088               childList: true,
1089               attributes: true,
1090               subtree: true,
1091               attributeOldValue: false,
1092               characterDataOldValue: false
1093             }));
1094       }
1095       if (!cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
1096         cvox.ChromeVox.navigationManager.updateSel(
1097             cvox.CursorSelection.fromNode(
1098                 cvox.ChromeVoxEventWatcher.currentTextControl));
1099       }
1100     }
1101
1102     return (null != cvox.ChromeVoxEventWatcher.currentTextHandler);
1103   }
1104 };
1105
1106 /**
1107  * Speaks updates to editable text controls as needed.
1108  *
1109  * @param {boolean} isKeypress Was this change triggered by a keypress?
1110  * @return {boolean} True if an editable text control has focus.
1111  */
1112 cvox.ChromeVoxEventWatcher.handleTextChanged = function(isKeypress) {
1113   if (cvox.ChromeVoxEventWatcher.currentTextHandler) {
1114     var handler = cvox.ChromeVoxEventWatcher.currentTextHandler;
1115     var shouldFlush = false;
1116     if (isKeypress && cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance) {
1117       shouldFlush = true;
1118       cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
1119     }
1120     handler.update(shouldFlush);
1121     cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
1122     return true;
1123   }
1124   return false;
1125 };
1126
1127 /**
1128  * Called when an editable text control has focus, because many changes
1129  * to a text box don't ever generate events - e.g. if the page's javascript
1130  * changes the contents of the text box after some delay, or if it's
1131  * contentEditable or a generic div with role="textbox".
1132  */
1133 cvox.ChromeVoxEventWatcher.onTextMutation = function() {
1134   if (cvox.ChromeVoxEventWatcher.currentTextHandler) {
1135     window.setTimeout(function() {
1136       cvox.ChromeVoxEventWatcher.handleTextChanged(false);
1137     }, cvox.ChromeVoxEventWatcher.MAX_WAIT_TIME_MS_);
1138   }
1139 };
1140
1141 /**
1142  * Speaks updates to other form controls as needed.
1143  * @param {Element} control The target control.
1144  */
1145 cvox.ChromeVoxEventWatcher.handleControlChanged = function(control) {
1146   var newValue = cvox.DomUtil.getControlValueAndStateString(control);
1147   var parentControl = cvox.DomUtil.getSurroundingControl(control);
1148   var announceChange = false;
1149
1150   if (control != cvox.ChromeVoxEventWatcher.lastFocusedNode &&
1151       (parentControl == null ||
1152        parentControl != cvox.ChromeVoxEventWatcher.lastFocusedNode)) {
1153     cvox.ChromeVoxEventWatcher.setLastFocusedNode_(control);
1154   } else if (newValue == cvox.ChromeVoxEventWatcher.lastFocusedNodeValue) {
1155     return;
1156   }
1157
1158   cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = newValue;
1159   if (cvox.DomPredicates.checkboxPredicate([control]) ||
1160       cvox.DomPredicates.radioPredicate([control])) {
1161     // Always announce changes to checkboxes and radio buttons.
1162     announceChange = true;
1163     // Play earcons for checkboxes and radio buttons
1164     if (control.checked) {
1165       cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.CHECK_ON);
1166     } else {
1167       cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.CHECK_OFF);
1168     }
1169   }
1170
1171   if (control.tagName == 'SELECT') {
1172     announceChange = true;
1173   }
1174
1175   if (control.tagName == 'INPUT') {
1176     switch (control.type) {
1177       case 'color':
1178       case 'datetime':
1179       case 'datetime-local':
1180       case 'range':
1181         announceChange = true;
1182         break;
1183       default:
1184         break;
1185     }
1186   }
1187
1188   // Always announce changes for anything with an ARIA role.
1189   if (control.hasAttribute && control.hasAttribute('role')) {
1190     announceChange = true;
1191   }
1192
1193   if ((parentControl &&
1194       parentControl != control &&
1195       document.activeElement == control)) {
1196     // If focus has been set on a child of the parent control, we need to
1197     // sync to that node so that ChromeVox navigation will be in sync with
1198     // focus navigation.
1199     cvox.ApiImplementation.syncToNode(
1200         control, true,
1201         cvox.ChromeVoxEventWatcher.queueMode_());
1202     announceChange = false;
1203   } else if (cvox.AriaUtil.getActiveDescendant(control)) {
1204     cvox.ChromeVox.navigationManager.updateSelToArbitraryNode(
1205         cvox.AriaUtil.getActiveDescendant(control),
1206         true);
1207
1208     announceChange = true;
1209   }
1210
1211   if (announceChange && !cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
1212     cvox.ChromeVox.tts.speak(newValue,
1213                              cvox.ChromeVoxEventWatcher.queueMode_(),
1214                              null);
1215     cvox.NavBraille.fromText(newValue).write();
1216   }
1217 };
1218
1219 /**
1220  * Handle actions on form controls triggered by key presses.
1221  * @param {Object} evt The event.
1222  * @return {boolean} True if this key event was handled.
1223  */
1224 cvox.ChromeVoxEventWatcher.handleControlAction = function(evt) {
1225   // Ignore the control action if ChromeVox is not active.
1226   if (!cvox.ChromeVox.isActive) {
1227     return false;
1228   }
1229   var control = evt.target;
1230
1231   if (control.tagName == 'SELECT' && (control.size <= 1) &&
1232       (evt.keyCode == 13 || evt.keyCode == 32)) { // Enter or Space
1233     // TODO (dmazzoni, clchen): Remove this workaround once accessibility
1234     // APIs make browser based popups accessible.
1235     //
1236     // Do nothing, but eat this keystroke when the SELECT control
1237     // has a dropdown style since if we don't, it will generate
1238     // a browser popup menu which is not accessible.
1239     // List style SELECT controls are fine and don't need this workaround.
1240     evt.preventDefault();
1241     evt.stopPropagation();
1242     return true;
1243   }
1244
1245   if (control.tagName == 'INPUT' && control.type == 'range') {
1246     var value = parseFloat(control.value);
1247     var step;
1248     if (control.step && control.step > 0.0) {
1249       step = control.step;
1250     } else if (control.min && control.max) {
1251       var range = (control.max - control.min);
1252       if (range > 2 && range < 31) {
1253         step = 1;
1254       } else {
1255         step = (control.max - control.min) / 10;
1256       }
1257     } else {
1258       step = 1;
1259     }
1260
1261     if (evt.keyCode == 37 || evt.keyCode == 38) {  // left or up
1262       value -= step;
1263     } else if (evt.keyCode == 39 || evt.keyCode == 40) {  // right or down
1264       value += step;
1265     }
1266
1267     if (control.max && value > control.max) {
1268       value = control.max;
1269     }
1270     if (control.min && value < control.min) {
1271       value = control.min;
1272     }
1273
1274     control.value = value;
1275   }
1276   return false;
1277 };
1278
1279 /**
1280  * When an element receives focus, see if we've entered or left a dialog
1281  * and return a string describing the event.
1282  *
1283  * @param {Element} target The element that just received focus.
1284  * @return {boolean} True if an announcement was spoken.
1285  */
1286 cvox.ChromeVoxEventWatcher.handleDialogFocus = function(target) {
1287   var dialog = target;
1288   var role = '';
1289   while (dialog) {
1290     if (dialog.hasAttribute) {
1291       role = dialog.getAttribute('role');
1292       if (role == 'dialog' || role == 'alertdialog') {
1293         break;
1294       }
1295     }
1296     dialog = dialog.parentElement;
1297   }
1298
1299   if (dialog == cvox.ChromeVox.navigationManager.currentDialog) {
1300     return false;
1301   }
1302
1303   if (cvox.ChromeVox.navigationManager.currentDialog && !dialog) {
1304     if (!cvox.DomUtil.isDescendantOfNode(
1305         document.activeElement,
1306         cvox.ChromeVox.navigationManager.currentDialog)) {
1307       cvox.ChromeVox.navigationManager.currentDialog = null;
1308
1309       cvox.ChromeVox.tts.speak(
1310           cvox.ChromeVox.msgs.getMsg('exiting_dialog'),
1311           cvox.ChromeVoxEventWatcher.queueMode_(),
1312           cvox.AbstractTts.PERSONALITY_ANNOTATION);
1313       return true;
1314     }
1315   } else {
1316     if (dialog) {
1317       cvox.ChromeVox.navigationManager.currentDialog = dialog;
1318       cvox.ChromeVox.tts.speak(
1319           cvox.ChromeVox.msgs.getMsg('entering_dialog'),
1320           cvox.ChromeVoxEventWatcher.queueMode_(),
1321           cvox.AbstractTts.PERSONALITY_ANNOTATION);
1322       if (role == 'alertdialog') {
1323         var dialogDescArray =
1324             cvox.DescriptionUtil.getFullDescriptionsFromChildren(null, dialog);
1325         var descSpeaker = new cvox.NavigationSpeaker();
1326         descSpeaker.speakDescriptionArray(dialogDescArray,
1327                                           cvox.AbstractTts.QUEUE_MODE_QUEUE,
1328                                           null);
1329       }
1330       return true;
1331     }
1332   }
1333   return false;
1334 };
1335
1336 /**
1337  * Returns true if we should wait to process events.
1338  * @param {number} lastFocusTimestamp The timestamp of the last focus event.
1339  * @param {number} firstTimestamp The timestamp of the first event.
1340  * @param {number} currentTime The current timestamp.
1341  * @return {boolean} True if we should wait to process events.
1342  */
1343 cvox.ChromeVoxEventWatcherUtil.shouldWaitToProcess = function(
1344     lastFocusTimestamp, firstTimestamp, currentTime) {
1345   var timeSinceFocusEvent = currentTime - lastFocusTimestamp;
1346   var timeSinceFirstEvent = currentTime - firstTimestamp;
1347   return timeSinceFocusEvent < cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_ &&
1348       timeSinceFirstEvent < cvox.ChromeVoxEventWatcher.MAX_WAIT_TIME_MS_;
1349 };
1350
1351
1352 /**
1353  * Returns the queue mode to use for the next utterance spoken as
1354  * a result of an event or navigation. The first utterance that's spoken
1355  * after an explicit user action like a key press will flush, and
1356  * subsequent events will return a category flush.
1357  * @return {number} Either QUEUE_MODE_FLUSH or QUEUE_MODE_QUEUE.
1358  * @private
1359  */
1360 cvox.ChromeVoxEventWatcher.queueMode_ = function() {
1361   if (cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance) {
1362     cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
1363     return cvox.AbstractTts.QUEUE_MODE_FLUSH;
1364   }
1365   return cvox.AbstractTts.QUEUE_MODE_CATEGORY_FLUSH;
1366 };
1367
1368
1369 /**
1370  * Processes the events queue.
1371  *
1372  * @private
1373  */
1374 cvox.ChromeVoxEventWatcher.processQueue_ = function() {
1375   // Return now if there are no events in the queue.
1376   if (cvox.ChromeVoxEventWatcher.events_.length == 0) {
1377     return;
1378   }
1379
1380   // Look for the most recent focus event and delete any preceding event
1381   // that applied to whatever was focused previously.
1382   var events = cvox.ChromeVoxEventWatcher.events_;
1383   var lastFocusIndex = -1;
1384   var lastFocusTimestamp = 0;
1385   var evt;
1386   var i;
1387   for (i = 0; evt = events[i]; i++) {
1388     if (evt.type == 'focus') {
1389       lastFocusIndex = i;
1390       lastFocusTimestamp = evt.timeStamp;
1391     }
1392   }
1393   cvox.ChromeVoxEventWatcher.events_ = [];
1394   for (i = 0; evt = events[i]; i++) {
1395     var prevEvt = events[i - 1] || {};
1396     if ((i >= lastFocusIndex || evt.type == 'LiveRegion' ||
1397         evt.type == 'DOMSubtreeModified') &&
1398         (prevEvt.type != 'focus' || evt.type != 'change')) {
1399       cvox.ChromeVoxEventWatcher.events_.push(evt);
1400     }
1401   }
1402
1403   cvox.ChromeVoxEventWatcher.events_.sort(function(a, b) {
1404     if (b.type != 'LiveRegion' && a.type == 'LiveRegion') {
1405       return 1;
1406     }
1407     if (b.type != 'DOMSubtreeModified' && a.type == 'DOMSubtreeModified') {
1408       return 1;
1409     }
1410     return -1;
1411   });
1412
1413   // If the most recent focus event was very recent, wait for things to
1414   // settle down before processing events, unless the max wait time has
1415   // passed.
1416   var currentTime = new Date().getTime();
1417   if (lastFocusIndex >= 0 &&
1418       cvox.ChromeVoxEventWatcherUtil.shouldWaitToProcess(
1419           lastFocusTimestamp,
1420           cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime,
1421           currentTime)) {
1422     window.setTimeout(cvox.ChromeVoxEventWatcher.processQueue_,
1423                       cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_);
1424     return;
1425   }
1426
1427   // Process the remaining events in the queue, in order.
1428   for (i = 0; evt = cvox.ChromeVoxEventWatcher.events_[i]; i++) {
1429     cvox.ChromeVoxEventWatcher.handleEvent_(evt);
1430   }
1431   cvox.ChromeVoxEventWatcher.events_ = new Array();
1432   cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime = -1;
1433   cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = false;
1434   cvox.ChromeVoxEventWatcher.maybeCallReadyCallbacks_();
1435 };
1436
1437 /**
1438  * Handle events from the queue by routing them to their respective handlers.
1439  *
1440  * @private
1441  * @param {Event} evt The event to be handled.
1442  */
1443 cvox.ChromeVoxEventWatcher.handleEvent_ = function(evt) {
1444   switch (evt.type) {
1445     case 'keydown':
1446     case 'input':
1447       cvox.ChromeVoxEventWatcher.setUpTextHandler();
1448       if (cvox.ChromeVoxEventWatcher.currentTextControl) {
1449         cvox.ChromeVoxEventWatcher.handleTextChanged(true);
1450
1451         var editableText = /** @type {cvox.ChromeVoxEditableTextBase} */
1452             (cvox.ChromeVoxEventWatcher.currentTextHandler);
1453         if (editableText && editableText.lastChangeDescribed) {
1454           break;
1455         }
1456       }
1457       // We're either not on a text control, or we are on a text control but no
1458       // text change was described. Let's try describing the state instead.
1459       cvox.ChromeVoxEventWatcher.handleControlChanged(document.activeElement);
1460       break;
1461     case 'keyup':
1462       // Some controls change only after key up.
1463       cvox.ChromeVoxEventWatcher.handleControlChanged(document.activeElement);
1464       break;
1465     case 'keypress':
1466       cvox.ChromeVoxEventWatcher.setUpTextHandler();
1467       break;
1468     case 'click':
1469       cvox.ApiImplementation.syncToNode(/** @type {Node} */(evt.target), true);
1470       break;
1471     case 'focus':
1472       cvox.ChromeVoxEventWatcher.focusHandler(evt);
1473       break;
1474     case 'blur':
1475       cvox.ChromeVoxEventWatcher.setUpTextHandler();
1476       break;
1477     case 'change':
1478       cvox.ChromeVoxEventWatcher.changeHandler(evt);
1479       break;
1480     case 'select':
1481       cvox.ChromeVoxEventWatcher.setUpTextHandler();
1482       break;
1483     case 'LiveRegion':
1484       cvox.ChromeVoxEventWatcher.speakLiveRegion_(
1485           evt.assertive, evt.navDescriptions);
1486       break;
1487     case 'DOMSubtreeModified':
1488       cvox.ChromeVoxEventWatcher.subtreeModifiedHandler(evt);
1489       break;
1490   }
1491 };
1492
1493
1494 /**
1495  * Sets up the time handler.
1496  * @return {boolean} True if a time control has focus.
1497  * @private
1498  */
1499 cvox.ChromeVoxEventWatcher.setUpTimeHandler_ = function() {
1500   var currentFocus = document.activeElement;
1501   if (currentFocus &&
1502       currentFocus.hasAttribute &&
1503       currentFocus.getAttribute('aria-hidden') == 'true' &&
1504       currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1505     currentFocus = null;
1506   }
1507   if (currentFocus.constructor == HTMLInputElement &&
1508       currentFocus.type && (currentFocus.type == 'time')) {
1509     cvox.ChromeVoxEventWatcher.currentTimeHandler =
1510         new cvox.ChromeVoxHTMLTimeWidget(currentFocus, cvox.ChromeVox.tts);
1511     } else {
1512       cvox.ChromeVoxEventWatcher.currentTimeHandler = null;
1513     }
1514   return (null != cvox.ChromeVoxEventWatcher.currentTimeHandler);
1515 };
1516
1517
1518 /**
1519  * Sets up the media (video/audio) handler.
1520  * @return {boolean} True if a media control has focus.
1521  * @private
1522  */
1523 cvox.ChromeVoxEventWatcher.setUpMediaHandler_ = function() {
1524   var currentFocus = document.activeElement;
1525   if (currentFocus &&
1526       currentFocus.hasAttribute &&
1527       currentFocus.getAttribute('aria-hidden') == 'true' &&
1528       currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1529     currentFocus = null;
1530   }
1531   if ((currentFocus.constructor == HTMLVideoElement) ||
1532       (currentFocus.constructor == HTMLAudioElement)) {
1533     cvox.ChromeVoxEventWatcher.currentMediaHandler =
1534         new cvox.ChromeVoxHTMLMediaWidget(currentFocus, cvox.ChromeVox.tts);
1535     } else {
1536       cvox.ChromeVoxEventWatcher.currentMediaHandler = null;
1537     }
1538   return (null != cvox.ChromeVoxEventWatcher.currentMediaHandler);
1539 };
1540
1541 /**
1542  * Sets up the date handler.
1543  * @return {boolean} True if a date control has focus.
1544  * @private
1545  */
1546 cvox.ChromeVoxEventWatcher.setUpDateHandler_ = function() {
1547   var currentFocus = document.activeElement;
1548   if (currentFocus &&
1549       currentFocus.hasAttribute &&
1550       currentFocus.getAttribute('aria-hidden') == 'true' &&
1551       currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1552     currentFocus = null;
1553   }
1554   if (currentFocus.constructor == HTMLInputElement &&
1555       currentFocus.type &&
1556       ((currentFocus.type == 'date') ||
1557       (currentFocus.type == 'month') ||
1558       (currentFocus.type == 'week'))) {
1559     cvox.ChromeVoxEventWatcher.currentDateHandler =
1560         new cvox.ChromeVoxHTMLDateWidget(currentFocus, cvox.ChromeVox.tts);
1561     } else {
1562       cvox.ChromeVoxEventWatcher.currentDateHandler = null;
1563     }
1564   return (null != cvox.ChromeVoxEventWatcher.currentDateHandler);
1565 };