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.NavigationSpeaker');
     30 goog.require('cvox.PlatformFilter');  // TODO: Find a better place for this.
     31 goog.require('cvox.PlatformUtil');
     32 goog.require('cvox.TextHandlerInterface');
     33 goog.require('cvox.UserEventDetail');
     34 
     35 /**
     36  * @constructor
     37  */
     38 cvox.ChromeVoxEventWatcher = function() {
     39 };
     40 
     41 /**
     42  * The maximum amount of time to wait before processing events.
     43  * A max time is needed so that even if a page is constantly updating,
     44  * events will still go through.
     45  * @const
     46  * @type {number}
     47  * @private
     48  */
     49 cvox.ChromeVoxEventWatcher.MAX_WAIT_TIME_MS_ = 50;
     50 
     51 /**
     52  * As long as the MAX_WAIT_TIME_ has not been exceeded, the event processor
     53  * will wait this long after the last event was received before starting to
     54  * process events.
     55  * @const
     56  * @type {number}
     57  * @private
     58  */
     59 cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_ = 10;
     60 
     61 /**
     62  * Amount of time in ms to wait before considering a subtree modified event to
     63  * be the start of a new burst of subtree modified events.
     64  * @const
     65  * @type {number}
     66  * @private
     67  */
     68 cvox.ChromeVoxEventWatcher.SUBTREE_MODIFIED_BURST_DURATION_ = 1000;
     69 
     70 
     71 /**
     72  * Number of subtree modified events that are part of the same burst to process
     73  * before we give up on processing any more events from that burst.
     74  * @const
     75  * @type {number}
     76  * @private
     77  */
     78 cvox.ChromeVoxEventWatcher.SUBTREE_MODIFIED_BURST_COUNT_LIMIT_ = 3;
     79 
     80 
     81 /**
     82  * Maximum number of live regions that we will attempt to process.
     83  * @const
     84  * @type {number}
     85  * @private
     86  */
     87 cvox.ChromeVoxEventWatcher.MAX_LIVE_REGIONS_ = 5;
     88 
     89 
     90 /**
     91  * Whether or not ChromeVox should echo keys.
     92  * It is useful to turn this off in case the system is already echoing keys (for
     93  * example, in Android).
     94  *
     95  * @type {boolean}
     96  */
     97 cvox.ChromeVoxEventWatcher.shouldEchoKeys = true;
     98 
     99 
    100 /**
    101  * Whether or not the next utterance should flush all previous speech.
    102  * Immediately after a key down or user action, we make the next speech
    103  * flush, but otherwise it's better to do a category flush, so if a single
    104  * user action generates both a focus change and a live region change,
    105  * both get spoken.
    106  * @type {boolean}
    107  */
    108 cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
    109 
    110 
    111 /**
    112  * Inits the event watcher and adds listeners.
    113  * @param {!Document|!Window} doc The DOM document to add event listeners to.
    114  */
    115 cvox.ChromeVoxEventWatcher.init = function(doc) {
    116   /**
    117    * @type {Object}
    118    */
    119   cvox.ChromeVoxEventWatcher.lastFocusedNode = null;
    120 
    121   /**
    122    * @type {Object}
    123    */
    124   cvox.ChromeVoxEventWatcher.announcedMouseOverNode = null;
    125 
    126   /**
    127    * @type {Object}
    128    */
    129   cvox.ChromeVoxEventWatcher.pendingMouseOverNode = null;
    130 
    131   /**
    132    * @type {number?}
    133    */
    134   cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
    135 
    136   /**
    137    * @type {string?}
    138    */
    139   cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = null;
    140 
    141   /**
    142    * @type {Object}
    143    */
    144   cvox.ChromeVoxEventWatcher.eventToEat = null;
    145 
    146   /**
    147    * @type {Element}
    148    */
    149   cvox.ChromeVoxEventWatcher.currentTextControl = null;
    150 
    151   /**
    152    * @type {cvox.ChromeVoxEditableTextBase}
    153    */
    154   cvox.ChromeVoxEventWatcher.currentTextHandler = null;
    155 
    156   /**
    157    * Array of event listeners we've added so we can unregister them if needed.
    158    * @type {Array}
    159    * @private
    160    */
    161   cvox.ChromeVoxEventWatcher.listeners_ = [];
    162 
    163   /**
    164    * The mutation observer we use to listen for live regions.
    165    * @type {MutationObserver}
    166    * @private
    167    */
    168   cvox.ChromeVoxEventWatcher.mutationObserver_ = null;
    169 
    170   /**
    171    * Whether or not mouse hover events should trigger focusing.
    172    * @type {boolean}
    173    */
    174   cvox.ChromeVoxEventWatcher.focusFollowsMouse = false;
    175 
    176   /**
    177    * The delay before a mouseover triggers focusing or announcing anything.
    178    * @type {number}
    179    */
    180   cvox.ChromeVoxEventWatcher.mouseoverDelayMs = 500;
    181 
    182   /**
    183    * Array of events that need to be processed.
    184    * @type {Array.<Event>}
    185    * @private
    186    */
    187   cvox.ChromeVoxEventWatcher.events_ = new Array();
    188 
    189   /**
    190    * The time when the last event was received.
    191    * @type {number}
    192    */
    193   cvox.ChromeVoxEventWatcher.lastEventTime = 0;
    194 
    195   /**
    196    * The timestamp for the first unprocessed event.
    197    * @type {number}
    198    */
    199   cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime = -1;
    200 
    201   /**
    202    * Whether or not queue processing is scheduled to run.
    203    * @type {boolean}
    204    * @private
    205    */
    206   cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = false;
    207 
    208   /**
    209    * A list of callbacks to be called when the EventWatcher has
    210    * completed processing all events in its queue.
    211    * @type {Array.<function()>}
    212    * @private
    213    */
    214   cvox.ChromeVoxEventWatcher.readyCallbacks_ = new Array();
    215 
    216 
    217 /**
    218  * tracks whether we've received two or more key up's while pass through mode
    219  * is active.
    220  * @type {boolean}
    221  * @private
    222  */
    223 cvox.ChromeVoxEventWatcher.secondPassThroughKeyUp_ = false;
    224 
    225   /**
    226    * Whether or not the ChromeOS Search key (keyCode == 91) is being held.
    227    *
    228    * We must track this manually because on ChromeOS, the Search key being held
    229    * down does not cause keyEvent.metaKey to be set.
    230    *
    231    * TODO (clchen, dmazzoni): Refactor this since there are edge cases
    232    * where manually tracking key down and key up can fail (such as when
    233    * the user switches tabs before letting go of the key being held).
    234    *
    235    * @type {boolean}
    236    */
    237   cvox.ChromeVox.searchKeyHeld = false;
    238 
    239   /**
    240    * The mutation observer that listens for chagnes to text controls
    241    * that might not send other events.
    242    * @type {MutationObserver}
    243    * @private
    244    */
    245   cvox.ChromeVoxEventWatcher.textMutationObserver_ = null;
    246 
    247   cvox.ChromeVoxEventWatcher.addEventListeners_(doc);
    248 
    249   /**
    250    * The time when the last burst of subtree modified events started
    251    * @type {number}
    252    * @private
    253    */
    254   cvox.ChromeVoxEventWatcher.lastSubtreeModifiedEventBurstTime_ = 0;
    255 
    256   /**
    257    * The number of subtree modified events in the current burst.
    258    * @type {number}
    259    * @private
    260    */
    261   cvox.ChromeVoxEventWatcher.subtreeModifiedEventsCount_ = 0;
    262 };
    263 
    264 
    265 /**
    266  * Stores state variables in a provided object.
    267  *
    268  * @param {Object} store The object.
    269  */
    270 cvox.ChromeVoxEventWatcher.storeOn = function(store) {
    271   store['searchKeyHeld'] = cvox.ChromeVox.searchKeyHeld;
    272 };
    273 
    274 /**
    275  * Updates the object with state variables from an earlier storeOn call.
    276  *
    277  * @param {Object} store The object.
    278  */
    279 cvox.ChromeVoxEventWatcher.readFrom = function(store) {
    280   cvox.ChromeVox.searchKeyHeld = store['searchKeyHeld'];
    281 };
    282 
    283 /**
    284  * Adds an event to the events queue and updates the time when the last
    285  * event was received.
    286  *
    287  * @param {Event} evt The event to be added to the events queue.
    288  * @param {boolean=} opt_ignoreVisibility Whether to ignore visibility
    289  * checking on the document. By default, this is set to false (so an
    290  * invisible document would result in this event not being added).
    291  */
    292 cvox.ChromeVoxEventWatcher.addEvent = function(evt, opt_ignoreVisibility) {
    293   // Don't add any events to the events queue if ChromeVox is inactive or the
    294   // page is hidden unless specified to not do so.
    295   if (!cvox.ChromeVox.isActive ||
    296       (document.webkitHidden && !opt_ignoreVisibility)) {
    297     return;
    298   }
    299   cvox.ChromeVoxEventWatcher.events_.push(evt);
    300   cvox.ChromeVoxEventWatcher.lastEventTime = new Date().getTime();
    301   if (cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime == -1) {
    302     cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime = new Date().getTime();
    303   }
    304   if (!cvox.ChromeVoxEventWatcher.queueProcessingScheduled_) {
    305     cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = true;
    306     window.setTimeout(cvox.ChromeVoxEventWatcher.processQueue_,
    307         cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_);
    308   }
    309 };
    310 
    311 /**
    312  * Adds a callback to be called when the event watcher has finished
    313  * processing all pending events.
    314  * @param {Function} cb The callback.
    315  */
    316 cvox.ChromeVoxEventWatcher.addReadyCallback = function(cb) {
    317   cvox.ChromeVoxEventWatcher.readyCallbacks_.push(cb);
    318   cvox.ChromeVoxEventWatcher.maybeCallReadyCallbacks_();
    319 };
    320 
    321 /**
    322  * Returns whether or not there are pending events.
    323  * @return {boolean} Whether or not there are pending events.
    324  * @private
    325  */
    326 cvox.ChromeVoxEventWatcher.hasPendingEvents_ = function() {
    327   return cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime != -1 ||
    328       cvox.ChromeVoxEventWatcher.queueProcessingScheduled_;
    329 };
    330 
    331 
    332 /**
    333  * A bit used to make sure only one ready callback is pending at a time.
    334  * @private
    335  */
    336 cvox.ChromeVoxEventWatcher.readyCallbackRunning_ = false;
    337 
    338 /**
    339  * Checks if the event watcher has pending events.  If not, call the oldest
    340  * readyCallback in a loop until exhausted or until there are pending events.
    341  * @private
    342  */
    343 cvox.ChromeVoxEventWatcher.maybeCallReadyCallbacks_ = function() {
    344   if (!cvox.ChromeVoxEventWatcher.readyCallbackRunning_) {
    345     cvox.ChromeVoxEventWatcher.readyCallbackRunning_ = true;
    346     window.setTimeout(function() {
    347       cvox.ChromeVoxEventWatcher.readyCallbackRunning_ = false;
    348       if (!cvox.ChromeVoxEventWatcher.hasPendingEvents_() &&
    349              !cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ &&
    350              cvox.ChromeVoxEventWatcher.readyCallbacks_.length > 0) {
    351         cvox.ChromeVoxEventWatcher.readyCallbacks_.shift()();
    352         cvox.ChromeVoxEventWatcher.maybeCallReadyCallbacks_();
    353       }
    354     }, 5);
    355   }
    356 };
    357 
    358 
    359 /**
    360  * Add all of our event listeners to the document.
    361  * @param {!Document|!Window} doc The DOM document to add event listeners to.
    362  * @private
    363  */
    364 cvox.ChromeVoxEventWatcher.addEventListeners_ = function(doc) {
    365   // We always need key down listeners to intercept activate/deactivate.
    366   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    367       'keydown', cvox.ChromeVoxEventWatcher.keyDownEventWatcher, true);
    368 
    369   // If ChromeVox isn't active, skip all other event listeners.
    370   if (!cvox.ChromeVox.isActive || cvox.ChromeVox.entireDocumentIsHidden) {
    371     return;
    372   }
    373   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    374       'keypress', cvox.ChromeVoxEventWatcher.keyPressEventWatcher, true);
    375   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    376       'keyup', cvox.ChromeVoxEventWatcher.keyUpEventWatcher, true);
    377   // Listen for our own events to handle public user commands if the web app
    378   // doesn't do it for us.
    379   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    380       cvox.UserEventDetail.Category.JUMP,
    381       cvox.ChromeVoxUserCommands.handleChromeVoxUserEvent,
    382       false);
    383 
    384   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    385       'focus', cvox.ChromeVoxEventWatcher.focusEventWatcher, true);
    386   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    387       'blur', cvox.ChromeVoxEventWatcher.blurEventWatcher, true);
    388   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    389       'change', cvox.ChromeVoxEventWatcher.changeEventWatcher, true);
    390   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    391       'copy', cvox.ChromeVoxEventWatcher.clipboardEventWatcher, true);
    392   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    393       'cut', cvox.ChromeVoxEventWatcher.clipboardEventWatcher, true);
    394   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    395       'paste', cvox.ChromeVoxEventWatcher.clipboardEventWatcher, true);
    396   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    397       'select', cvox.ChromeVoxEventWatcher.selectEventWatcher, true);
    398 
    399   // TODO(dtseng): Experimental, see:
    400   // https://developers.google.com/chrome/whitepapers/pagevisibility
    401   cvox.ChromeVoxEventWatcher.addEventListener_(doc, 'webkitvisibilitychange',
    402       cvox.ChromeVoxEventWatcher.visibilityChangeWatcher, true);
    403   cvox.ChromeVoxEventWatcher.events_ = new Array();
    404   cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = false;
    405 
    406   // Handle mouse events directly without going into the events queue.
    407   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    408       'mouseover', cvox.ChromeVoxEventWatcher.mouseOverEventWatcher, true);
    409   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    410       'mouseout', cvox.ChromeVoxEventWatcher.mouseOutEventWatcher, true);
    411 
    412   // With the exception of non-Android, click events go through the event queue.
    413   cvox.ChromeVoxEventWatcher.addEventListener_(doc,
    414       'click', cvox.ChromeVoxEventWatcher.mouseClickEventWatcher, true);
    415 
    416   if (typeof(window.WebKitMutationObserver) != 'undefined') {
    417     cvox.ChromeVoxEventWatcher.mutationObserver_ =
    418         new window.WebKitMutationObserver(
    419             cvox.ChromeVoxEventWatcher.mutationHandler);
    420     var observerTarget = null;
    421     if (doc.documentElement) {
    422       observerTarget = doc.documentElement;
    423     } else if (doc.document && doc.document.documentElement) {
    424       observerTarget = doc.document.documentElement;
    425     }
    426     if (observerTarget) {
    427       cvox.ChromeVoxEventWatcher.mutationObserver_.observe(
    428           observerTarget,
    429           /** @type {!MutationObserverInit} */ ({
    430             childList: true,
    431             attributes: true,
    432             characterData: true,
    433             subtree: true,
    434             attributeOldValue: true,
    435             characterDataOldValue: true
    436           }));
    437     }
    438   } else {
    439     cvox.ChromeVoxEventWatcher.addEventListener_(doc, 'DOMSubtreeModified',
    440         cvox.ChromeVoxEventWatcher.subtreeModifiedEventWatcher, true);
    441   }
    442 };
    443 
    444 
    445 /**
    446  * Remove all registered event watchers.
    447  * @param {!Document|!Window} doc The DOM document to add event listeners to.
    448  */
    449 cvox.ChromeVoxEventWatcher.cleanup = function(doc) {
    450   for (var i = 0; i < cvox.ChromeVoxEventWatcher.listeners_.length; i++) {
    451     var listener = cvox.ChromeVoxEventWatcher.listeners_[i];
    452     doc.removeEventListener(
    453         listener.type, listener.listener, listener.useCapture);
    454   }
    455   cvox.ChromeVoxEventWatcher.listeners_ = [];
    456   if (cvox.ChromeVoxEventWatcher.currentDateHandler) {
    457     cvox.ChromeVoxEventWatcher.currentDateHandler.shutdown();
    458   }
    459   if (cvox.ChromeVoxEventWatcher.currentTimeHandler) {
    460     cvox.ChromeVoxEventWatcher.currentTimeHandler.shutdown();
    461   }
    462   if (cvox.ChromeVoxEventWatcher.currentMediaHandler) {
    463     cvox.ChromeVoxEventWatcher.currentMediaHandler.shutdown();
    464   }
    465   if (cvox.ChromeVoxEventWatcher.mutationObserver_) {
    466     cvox.ChromeVoxEventWatcher.mutationObserver_.disconnect();
    467   }
    468   cvox.ChromeVoxEventWatcher.mutationObserver_ = null;
    469 };
    470 
    471 /**
    472  * Add one event listener and save the data so it can be removed later.
    473  * @param {!Document|!Window} doc The DOM document to add event listeners to.
    474  * @param {string} type The event type.
    475  * @param {EventListener|function(Event):(boolean|undefined)} listener
    476  *     The function to be called when the event is fired.
    477  * @param {boolean} useCapture Whether this listener should capture events
    478  *     before they're sent to targets beneath it in the DOM tree.
    479  * @private
    480  */
    481 cvox.ChromeVoxEventWatcher.addEventListener_ = function(doc, type,
    482     listener, useCapture) {
    483   cvox.ChromeVoxEventWatcher.listeners_.push(
    484       {'type': type, 'listener': listener, 'useCapture': useCapture});
    485   doc.addEventListener(type, listener, useCapture);
    486 };
    487 
    488 /**
    489  * Return the last focused node.
    490  * @return {Object} The last node that was focused.
    491  */
    492 cvox.ChromeVoxEventWatcher.getLastFocusedNode = function() {
    493   return cvox.ChromeVoxEventWatcher.lastFocusedNode;
    494 };
    495 
    496 /**
    497  * Sets the last focused node.
    498  * @param {Element} element The last focused element.
    499  *
    500  * @private.
    501  */
    502 cvox.ChromeVoxEventWatcher.setLastFocusedNode_ = function(element) {
    503   cvox.ChromeVoxEventWatcher.lastFocusedNode = element;
    504   cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = !element ? null :
    505       cvox.DomUtil.getControlValueAndStateString(element);
    506 };
    507 
    508 /**
    509  * Called when there's any mutation of the document. We use this to
    510  * handle live region updates.
    511  * @param {Array.<MutationRecord>} mutations The mutations.
    512  * @return {boolean} True if the default action should be performed.
    513  */
    514 cvox.ChromeVoxEventWatcher.mutationHandler = function(mutations) {
    515   if (cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
    516     return true;
    517   }
    518 
    519   cvox.ChromeVox.navigationManager.updateIndicatorIfChanged();
    520 
    521   cvox.LiveRegions.processMutations(
    522       mutations,
    523       function(assertive, navDescriptions) {
    524         var evt = new window.Event('LiveRegion');
    525         evt.navDescriptions = navDescriptions;
    526         evt.assertive = assertive;
    527         cvox.ChromeVoxEventWatcher.addEvent(evt, true);
    528         return true;
    529       });
    530 };
    531 
    532 
    533 /**
    534  * Handles mouseclick events.
    535  * Mouseclick events are only triggered if the user touches the mouse;
    536  * we use it to determine whether or not we should bother trying to sync to a
    537  * selection.
    538  * @param {Event} evt The mouseclick event to process.
    539  * @return {boolean} True if the default action should be performed.
    540  */
    541 cvox.ChromeVoxEventWatcher.mouseClickEventWatcher = function(evt) {
    542   if (evt.fromCvox) {
    543     return true;
    544   }
    545 
    546   if (cvox.ChromeVox.host.mustRedispatchClickEvent()) {
    547     cvox.ChromeVoxUserCommands.wasMouseClicked = true;
    548     evt.stopPropagation();
    549     evt.preventDefault();
    550     // Since the click event was caught and we are re-dispatching it, we also
    551     // need to refocus the current node because the current node has already
    552     // been blurred by the window getting the click event in the first place.
    553     // Failing to restore focus before clicking can cause odd problems such as
    554     // the soft IME not coming up in Android (it only shows up if the click
    555     // happens in a focused text field).
    556     cvox.Focuser.setFocus(cvox.ChromeVox.navigationManager.getCurrentNode());
    557     cvox.ChromeVox.tts.speak(
    558         cvox.ChromeVox.msgs.getMsg('element_clicked'),
    559         cvox.ChromeVoxEventWatcher.queueMode_(),
    560         cvox.AbstractTts.PERSONALITY_ANNOTATION);
    561     var targetNode = cvox.ChromeVox.navigationManager.getCurrentNode();
    562     // If the targetNode has a defined onclick function, just call it directly
    563     // rather than try to generate a click event and dispatching it.
    564     // While both work equally well on standalone Chrome, when dealing with
    565     // embedded WebViews, generating a click event and sending it is not always
    566     // reliable since the framework may swallow the event.
    567     cvox.DomUtil.clickElem(targetNode, false, true);
    568     return false;
    569   } else {
    570     cvox.ChromeVoxEventWatcher.addEvent(evt);
    571   }
    572   cvox.ChromeVoxUserCommands.wasMouseClicked = true;
    573   return true;
    574 };
    575 
    576 /**
    577  * Handles mouseover events.
    578  * Mouseover events are only triggered if the user touches the mouse, so
    579  * for users who only use the keyboard, this will have no effect.
    580  *
    581  * @param {Event} evt The mouseover event to process.
    582  * @return {boolean} True if the default action should be performed.
    583  */
    584 cvox.ChromeVoxEventWatcher.mouseOverEventWatcher = function(evt) {
    585   // 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  */
    643 cvox.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  */
    662 cvox.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  */
    682 cvox.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  */
    754 cvox.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  */
    770 cvox.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  */
    812 cvox.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  */
    850 cvox.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  */
    884 cvox.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  */
    896 cvox.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  */
    918 cvox.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  */
    933 cvox.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  */
    944 cvox.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  */
    955 cvox.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  */
    967 cvox.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  */
    980 cvox.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  */
    993 cvox.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  */
   1032 cvox.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  */
   1115 cvox.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  */
   1136 cvox.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  */
   1148 cvox.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  */
   1227 cvox.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  */
   1289 cvox.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  */
   1346 cvox.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  */
   1363 cvox.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  */
   1377 cvox.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  */
   1446 cvox.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  */
   1502 cvox.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  */
   1526 cvox.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  */
   1549 cvox.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