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