event_watcher.js revision 116680a4aac90f2aa7413d9095a592090648e557
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
10goog.provide('cvox.ChromeVoxEventWatcher');
11goog.provide('cvox.ChromeVoxEventWatcherUtil');
12
13goog.require('cvox.ActiveIndicator');
14goog.require('cvox.ApiImplementation');
15goog.require('cvox.AriaUtil');
16goog.require('cvox.ChromeVox');
17goog.require('cvox.ChromeVoxEditableTextBase');
18goog.require('cvox.ChromeVoxEventSuspender');
19goog.require('cvox.ChromeVoxHTMLDateWidget');
20goog.require('cvox.ChromeVoxHTMLMediaWidget');
21goog.require('cvox.ChromeVoxHTMLTimeWidget');
22goog.require('cvox.ChromeVoxKbHandler');
23goog.require('cvox.ChromeVoxUserCommands');
24goog.require('cvox.DomUtil');
25goog.require('cvox.Focuser');
26goog.require('cvox.History');
27goog.require('cvox.LiveRegions');
28goog.require('cvox.LiveRegionsDeprecated');
29goog.require('cvox.NavigationSpeaker');
30goog.require('cvox.PlatformFilter');  // TODO: Find a better place for this.
31goog.require('cvox.PlatformUtil');
32goog.require('cvox.TextHandlerInterface');
33goog.require('cvox.UserEventDetail');
34
35/**
36 * @constructor
37 */
38cvox.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 */
49cvox.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 */
59cvox.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 */
68cvox.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 */
78cvox.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 */
87cvox.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 */
97cvox.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 */
108cvox.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 */
115cvox.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 */
223cvox.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 */
270cvox.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 */
279cvox.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 */
292cvox.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 */
316cvox.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 */
326cvox.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 */
336cvox.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 */
343cvox.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 */
364cvox.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 */
449cvox.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 */
481cvox.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 */
492cvox.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 */
502cvox.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 */
514cvox.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 */
541cvox.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 */
584cvox.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 */
640cvox.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 */
659cvox.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 */
679cvox.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 */
751cvox.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 */
767cvox.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 */
809cvox.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 */
847cvox.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 */
881cvox.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 */
893cvox.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 */
915cvox.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 */
930cvox.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 */
941cvox.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 */
952cvox.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 */
964cvox.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 */
977cvox.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 */
990cvox.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 */
1029cvox.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 */
1112cvox.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 */
1133cvox.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 */
1145cvox.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 */
1224cvox.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 */
1286cvox.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 */
1343cvox.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 */
1360cvox.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 */
1374cvox.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 */
1443cvox.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 */
1499cvox.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 */
1523cvox.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 */
1546cvox.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};
1566