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