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