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.
6 * @fileoverview Watches for events in the browser such as focus changes.
10 goog.provide('cvox.ChromeVoxEventWatcher');
11 goog.provide('cvox.ChromeVoxEventWatcherUtil');
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');
38 cvox.ChromeVoxEventWatcher = function() {
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.
49 cvox.ChromeVoxEventWatcher.MAX_WAIT_TIME_MS_ = 50;
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
59 cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_ = 10;
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.
68 cvox.ChromeVoxEventWatcher.SUBTREE_MODIFIED_BURST_DURATION_ = 1000;
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.
78 cvox.ChromeVoxEventWatcher.SUBTREE_MODIFIED_BURST_COUNT_LIMIT_ = 3;
82 * Maximum number of live regions that we will attempt to process.
87 cvox.ChromeVoxEventWatcher.MAX_LIVE_REGIONS_ = 5;
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).
97 cvox.ChromeVoxEventWatcher.shouldEchoKeys = true;
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,
108 cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
112 * Inits the event watcher and adds listeners.
113 * @param {!Document|!Window} doc The DOM document to add event listeners to.
115 cvox.ChromeVoxEventWatcher.init = function(doc) {
119 cvox.ChromeVoxEventWatcher.lastFocusedNode = null;
124 cvox.ChromeVoxEventWatcher.announcedMouseOverNode = null;
129 cvox.ChromeVoxEventWatcher.pendingMouseOverNode = null;
134 cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
139 cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = null;
144 cvox.ChromeVoxEventWatcher.eventToEat = null;
149 cvox.ChromeVoxEventWatcher.currentTextControl = null;
152 * @type {cvox.ChromeVoxEditableTextBase}
154 cvox.ChromeVoxEventWatcher.currentTextHandler = null;
157 * Array of event listeners we've added so we can unregister them if needed.
161 cvox.ChromeVoxEventWatcher.listeners_ = [];
164 * The mutation observer we use to listen for live regions.
165 * @type {MutationObserver}
168 cvox.ChromeVoxEventWatcher.mutationObserver_ = null;
171 * Whether or not mouse hover events should trigger focusing.
174 cvox.ChromeVoxEventWatcher.focusFollowsMouse = false;
177 * The delay before a mouseover triggers focusing or announcing anything.
180 cvox.ChromeVoxEventWatcher.mouseoverDelayMs = 500;
183 * Array of events that need to be processed.
184 * @type {Array.<Event>}
187 cvox.ChromeVoxEventWatcher.events_ = new Array();
190 * The time when the last event was received.
193 cvox.ChromeVoxEventWatcher.lastEventTime = 0;
196 * The timestamp for the first unprocessed event.
199 cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime = -1;
202 * Whether or not queue processing is scheduled to run.
206 cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = false;
209 * A list of callbacks to be called when the EventWatcher has
210 * completed processing all events in its queue.
211 * @type {Array.<function()>}
214 cvox.ChromeVoxEventWatcher.readyCallbacks_ = new Array();
218 * tracks whether we've received two or more key up's while pass through mode
223 cvox.ChromeVoxEventWatcher.secondPassThroughKeyUp_ = false;
226 * Whether or not the ChromeOS Search key (keyCode == 91) is being held.
228 * We must track this manually because on ChromeOS, the Search key being held
229 * down does not cause keyEvent.metaKey to be set.
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).
237 cvox.ChromeVox.searchKeyHeld = false;
240 * The mutation observer that listens for chagnes to text controls
241 * that might not send other events.
242 * @type {MutationObserver}
245 cvox.ChromeVoxEventWatcher.textMutationObserver_ = null;
247 cvox.ChromeVoxEventWatcher.addEventListeners_(doc);
250 * The time when the last burst of subtree modified events started
254 cvox.ChromeVoxEventWatcher.lastSubtreeModifiedEventBurstTime_ = 0;
257 * The number of subtree modified events in the current burst.
261 cvox.ChromeVoxEventWatcher.subtreeModifiedEventsCount_ = 0;
266 * Stores state variables in a provided object.
268 * @param {Object} store The object.
270 cvox.ChromeVoxEventWatcher.storeOn = function(store) {
271 store['searchKeyHeld'] = cvox.ChromeVox.searchKeyHeld;
275 * Updates the object with state variables from an earlier storeOn call.
277 * @param {Object} store The object.
279 cvox.ChromeVoxEventWatcher.readFrom = function(store) {
280 cvox.ChromeVox.searchKeyHeld = store['searchKeyHeld'];
284 * Adds an event to the events queue and updates the time when the last
285 * event was received.
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).
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)) {
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();
304 if (!cvox.ChromeVoxEventWatcher.queueProcessingScheduled_) {
305 cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = true;
306 window.setTimeout(cvox.ChromeVoxEventWatcher.processQueue_,
307 cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_);
312 * Adds a callback to be called when the event watcher has finished
313 * processing all pending events.
314 * @param {Function} cb The callback.
316 cvox.ChromeVoxEventWatcher.addReadyCallback = function(cb) {
317 cvox.ChromeVoxEventWatcher.readyCallbacks_.push(cb);
318 cvox.ChromeVoxEventWatcher.maybeCallReadyCallbacks_();
322 * Returns whether or not there are pending events.
323 * @return {boolean} Whether or not there are pending events.
326 cvox.ChromeVoxEventWatcher.hasPendingEvents_ = function() {
327 return cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime != -1 ||
328 cvox.ChromeVoxEventWatcher.queueProcessingScheduled_;
333 * A bit used to make sure only one ready callback is pending at a time.
336 cvox.ChromeVoxEventWatcher.readyCallbackRunning_ = false;
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.
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_();
360 * Add all of our event listeners to the document.
361 * @param {!Document|!Window} doc The DOM document to add event listeners to.
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);
369 // If ChromeVox isn't active, skip all other event listeners.
370 if (!cvox.ChromeVox.isActive || cvox.ChromeVox.entireDocumentIsHidden) {
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,
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);
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;
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);
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);
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;
426 if (observerTarget) {
427 cvox.ChromeVoxEventWatcher.mutationObserver_.observe(
429 /** @type {!MutationObserverInit} */ ({
434 attributeOldValue: true,
435 characterDataOldValue: true
439 cvox.ChromeVoxEventWatcher.addEventListener_(doc, 'DOMSubtreeModified',
440 cvox.ChromeVoxEventWatcher.subtreeModifiedEventWatcher, true);
446 * Remove all registered event watchers.
447 * @param {!Document|!Window} doc The DOM document to add event listeners to.
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);
455 cvox.ChromeVoxEventWatcher.listeners_ = [];
456 if (cvox.ChromeVoxEventWatcher.currentDateHandler) {
457 cvox.ChromeVoxEventWatcher.currentDateHandler.shutdown();
459 if (cvox.ChromeVoxEventWatcher.currentTimeHandler) {
460 cvox.ChromeVoxEventWatcher.currentTimeHandler.shutdown();
462 if (cvox.ChromeVoxEventWatcher.currentMediaHandler) {
463 cvox.ChromeVoxEventWatcher.currentMediaHandler.shutdown();
465 if (cvox.ChromeVoxEventWatcher.mutationObserver_) {
466 cvox.ChromeVoxEventWatcher.mutationObserver_.disconnect();
468 cvox.ChromeVoxEventWatcher.mutationObserver_ = null;
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.
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);
489 * Return the last focused node.
490 * @return {Object} The last node that was focused.
492 cvox.ChromeVoxEventWatcher.getLastFocusedNode = function() {
493 return cvox.ChromeVoxEventWatcher.lastFocusedNode;
497 * Sets the last focused node.
498 * @param {Element} element The last focused element.
502 cvox.ChromeVoxEventWatcher.setLastFocusedNode_ = function(element) {
503 cvox.ChromeVoxEventWatcher.lastFocusedNode = element;
504 cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = !element ? null :
505 cvox.DomUtil.getControlValueAndStateString(element);
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.
514 cvox.ChromeVoxEventWatcher.mutationHandler = function(mutations) {
515 if (cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
519 cvox.ChromeVox.navigationManager.updateIndicatorIfChanged();
521 cvox.LiveRegions.processMutations(
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);
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
538 * @param {Event} evt The mouseclick event to process.
539 * @return {boolean} True if the default action should be performed.
541 cvox.ChromeVoxEventWatcher.mouseClickEventWatcher = function(evt) {
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);
570 cvox.ChromeVoxEventWatcher.addEvent(evt);
572 cvox.ChromeVoxUserCommands.wasMouseClicked = true;
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.
581 * @param {Event} evt The mouseover event to process.
582 * @return {boolean} True if the default action should be performed.
584 cvox.ChromeVoxEventWatcher.mouseOverEventWatcher = function(evt) {
585 var hasTouch = 'ontouchstart' in window;
586 var mouseoverDelayMs = cvox.ChromeVoxEventWatcher.mouseoverDelayMs;
588 mouseoverDelayMs = 0;
589 } else if (!cvox.ChromeVoxEventWatcher.focusFollowsMouse) {
593 if (cvox.DomUtil.isDescendantOfNode(
594 cvox.ChromeVoxEventWatcher.announcedMouseOverNode, evt.target)) {
598 if (evt.target == cvox.ChromeVoxEventWatcher.pendingMouseOverNode) {
602 cvox.ChromeVoxEventWatcher.pendingMouseOverNode = evt.target;
603 if (cvox.ChromeVoxEventWatcher.mouseOverTimeoutId) {
604 window.clearTimeout(cvox.ChromeVoxEventWatcher.mouseOverTimeoutId);
605 cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
608 if (evt.target.tagName && (evt.target.tagName == 'BODY')) {
609 cvox.ChromeVoxEventWatcher.pendingMouseOverNode = null;
610 cvox.ChromeVoxEventWatcher.announcedMouseOverNode = null;
614 // Only focus and announce if the mouse stays over the same target
615 // for longer than the given delay.
616 cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = window.setTimeout(
618 cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
619 if (evt.target != cvox.ChromeVoxEventWatcher.pendingMouseOverNode) {
622 cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = true;
623 cvox.ChromeVox.navigationManager.stopReading(true);
624 var target = /** @type {Node} */(evt.target);
625 cvox.Focuser.setFocus(target);
626 cvox.ApiImplementation.syncToNode(
627 target, true, cvox.ChromeVoxEventWatcher.queueMode_());
628 cvox.ChromeVoxEventWatcher.announcedMouseOverNode = target;
629 }, mouseoverDelayMs);
635 * Handles mouseout events.
637 * @param {Event} evt The mouseout event to process.
638 * @return {boolean} True if the default action should be performed.
640 cvox.ChromeVoxEventWatcher.mouseOutEventWatcher = function(evt) {
641 if (evt.target == cvox.ChromeVoxEventWatcher.pendingMouseOverNode) {
642 cvox.ChromeVoxEventWatcher.pendingMouseOverNode = null;
643 if (cvox.ChromeVoxEventWatcher.mouseOverTimeoutId) {
644 window.clearTimeout(cvox.ChromeVoxEventWatcher.mouseOverTimeoutId);
645 cvox.ChromeVoxEventWatcher.mouseOverTimeoutId = null;
654 * Watches for focus events.
656 * @param {Event} evt The focus event to add to the queue.
657 * @return {boolean} True if the default action should be performed.
659 cvox.ChromeVoxEventWatcher.focusEventWatcher = function(evt) {
660 // First remove any dummy spans. We create dummy spans in UserCommands in
661 // order to sync the browser's default tab action with the user's current
662 // navigation position.
663 cvox.ChromeVoxUserCommands.removeTabDummySpan();
665 if (!cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
666 cvox.ChromeVoxEventWatcher.addEvent(evt);
667 } else if (evt.target && evt.target.nodeType == Node.ELEMENT_NODE) {
668 cvox.ChromeVoxEventWatcher.setLastFocusedNode_(
669 /** @type {Element} */(evt.target));
675 * Handles for focus events passed to it from the events queue.
677 * @param {Event} evt The focus event to handle.
679 cvox.ChromeVoxEventWatcher.focusHandler = function(evt) {
681 evt.target.hasAttribute &&
682 evt.target.getAttribute('aria-hidden') == 'true' &&
683 evt.target.getAttribute('chromevoxignoreariahidden') != 'true') {
684 cvox.ChromeVoxEventWatcher.setLastFocusedNode_(null);
685 cvox.ChromeVoxEventWatcher.setUpTextHandler();
688 if (evt.target && evt.target != window) {
689 var target = /** @type {Element} */(evt.target);
690 var parentControl = cvox.DomUtil.getSurroundingControl(target);
692 parentControl == cvox.ChromeVoxEventWatcher.lastFocusedNode) {
693 cvox.ChromeVoxEventWatcher.handleControlChanged(target);
698 cvox.ChromeVoxEventWatcher.setLastFocusedNode_(
699 /** @type {Element} */(parentControl));
701 cvox.ChromeVoxEventWatcher.setLastFocusedNode_(target);
704 var queueMode = cvox.ChromeVoxEventWatcher.queueMode_();
706 if (cvox.ChromeVoxEventWatcher.getInitialVisibility() ||
707 cvox.ChromeVoxEventWatcher.handleDialogFocus(target)) {
708 queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
711 if (cvox.ChromeVox.navigationManager.clearPageSel(true)) {
712 queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
715 // Navigate to this control so that it will be the same for focus as for
716 // regular navigation.
717 cvox.ApiImplementation.syncToNode(
718 target, !document.webkitHidden, queueMode);
720 if ((evt.target.constructor == HTMLVideoElement) ||
721 (evt.target.constructor == HTMLAudioElement)) {
722 cvox.ChromeVoxEventWatcher.setUpMediaHandler_();
725 if (evt.target.hasAttribute) {
726 var inputType = evt.target.getAttribute('type');
729 cvox.ChromeVoxEventWatcher.setUpTimeHandler_();
734 cvox.ChromeVoxEventWatcher.setUpDateHandler_();
738 cvox.ChromeVoxEventWatcher.setUpTextHandler();
740 cvox.ChromeVoxEventWatcher.setLastFocusedNode_(null);
746 * Watches for blur events.
748 * @param {Event} evt The blur event to add to the queue.
749 * @return {boolean} True if the default action should be performed.
751 cvox.ChromeVoxEventWatcher.blurEventWatcher = function(evt) {
752 window.setTimeout(function() {
753 if (!document.activeElement) {
754 cvox.ChromeVoxEventWatcher.setLastFocusedNode_(null);
755 cvox.ChromeVoxEventWatcher.addEvent(evt);
762 * Watches for key down events.
764 * @param {Event} evt The keydown event to add to the queue.
765 * @return {boolean} True if the default action should be performed.
767 cvox.ChromeVoxEventWatcher.keyDownEventWatcher = function(evt) {
768 cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = true;
770 if (cvox.ChromeVox.passThroughMode) {
774 if (cvox.ChromeVox.isChromeOS && evt.keyCode == 91) {
775 cvox.ChromeVox.searchKeyHeld = true;
778 // Store some extra ChromeVox-specific properties in the event.
780 cvox.ChromeVox.searchKeyHeld && cvox.ChromeVox.isActive;
781 evt.stickyMode = cvox.ChromeVox.isStickyModeOn() && cvox.ChromeVox.isActive;
782 evt.keyPrefix = cvox.ChromeVox.keyPrefixOn && cvox.ChromeVox.isActive;
784 cvox.ChromeVox.keyPrefixOn = false;
786 cvox.ChromeVoxEventWatcher.eventToEat = null;
787 if (!cvox.ChromeVoxKbHandler.basicKeyDownActionsListener(evt) ||
788 cvox.ChromeVoxEventWatcher.handleControlAction(evt)) {
789 // Swallow the event immediately to prevent the arrow keys
790 // from driving controls on the web page.
791 evt.preventDefault();
792 evt.stopPropagation();
793 // Also mark this as something to be swallowed when the followup
794 // keypress/keyup counterparts to this event show up later.
795 cvox.ChromeVoxEventWatcher.eventToEat = evt;
798 cvox.ChromeVoxEventWatcher.addEvent(evt);
803 * Watches for key up events.
805 * @param {Event} evt The event to add to the queue.
806 * @return {boolean} True if the default action should be performed.
807 * @this {cvox.ChromeVoxEventWatcher}
809 cvox.ChromeVoxEventWatcher.keyUpEventWatcher = function(evt) {
810 if (evt.keyCode == 91) {
811 cvox.ChromeVox.searchKeyHeld = false;
814 if (cvox.ChromeVox.passThroughMode) {
815 if (!evt.ctrlKey && !evt.altKey && !evt.metaKey && !evt.shiftKey &&
816 !cvox.ChromeVox.searchKeyHeld) {
817 // Only reset pass through on the second key up without modifiers since
818 // the first one is from the pass through shortcut itself.
819 if (this.secondPassThroughKeyUp_) {
820 this.secondPassThroughKeyUp_ = false;
821 cvox.ChromeVox.passThroughMode = false;
823 this.secondPassThroughKeyUp_ = true;
829 if (cvox.ChromeVoxEventWatcher.eventToEat &&
830 evt.keyCode == cvox.ChromeVoxEventWatcher.eventToEat.keyCode) {
831 evt.stopPropagation();
832 evt.preventDefault();
836 cvox.ChromeVoxEventWatcher.addEvent(evt);
842 * Watches for key press events.
844 * @param {Event} evt The event to add to the queue.
845 * @return {boolean} True if the default action should be performed.
847 cvox.ChromeVoxEventWatcher.keyPressEventWatcher = function(evt) {
848 var url = document.location.href;
849 // Use ChromeVox.typingEcho as default value.
850 var speakChar = cvox.TypingEcho.shouldSpeakChar(cvox.ChromeVox.typingEcho);
852 if (typeof cvox.ChromeVox.keyEcho[url] !== 'undefined') {
853 speakChar = cvox.ChromeVox.keyEcho[url];
856 // Directly handle typed characters here while key echo is on. This
857 // skips potentially costly computations (especially for content editable).
858 // This is done deliberately for the sake of responsiveness and in some cases
859 // (e.g. content editable), to have characters echoed properly.
860 if (cvox.ChromeVoxEditableTextBase.eventTypingEcho && (speakChar &&
861 cvox.DomPredicates.editTextPredicate([document.activeElement])) &&
862 document.activeElement.type !== 'password') {
863 cvox.ChromeVox.tts.speak(String.fromCharCode(evt.charCode), 0);
865 cvox.ChromeVoxEventWatcher.addEvent(evt);
866 if (cvox.ChromeVoxEventWatcher.eventToEat &&
867 evt.keyCode == cvox.ChromeVoxEventWatcher.eventToEat.keyCode) {
868 evt.preventDefault();
869 evt.stopPropagation();
876 * Watches for change events.
878 * @param {Event} evt The event to add to the queue.
879 * @return {boolean} True if the default action should be performed.
881 cvox.ChromeVoxEventWatcher.changeEventWatcher = function(evt) {
882 cvox.ChromeVoxEventWatcher.addEvent(evt);
886 // TODO(dtseng): ChromeVoxEditableText interrupts cut and paste announcements.
888 * Watches for cut, copy, and paste events.
890 * @param {Event} evt The event to process.
891 * @return {boolean} True if the default action should be performed.
893 cvox.ChromeVoxEventWatcher.clipboardEventWatcher = function(evt) {
894 cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg(evt.type).toLowerCase());
898 text = evt.clipboardData.getData('text');
902 text = window.getSelection().toString();
905 cvox.ChromeVox.tts.speak(text, cvox.AbstractTts.QUEUE_MODE_QUEUE);
906 cvox.ChromeVox.navigationManager.clearPageSel();
911 * Handles change events passed to it from the events queue.
913 * @param {Event} evt The event to handle.
915 cvox.ChromeVoxEventWatcher.changeHandler = function(evt) {
916 if (cvox.ChromeVoxEventWatcher.setUpTextHandler()) {
919 if (document.activeElement == evt.target) {
920 cvox.ChromeVoxEventWatcher.handleControlChanged(document.activeElement);
925 * Watches for select events.
927 * @param {Event} evt The event to add to the queue.
928 * @return {boolean} True if the default action should be performed.
930 cvox.ChromeVoxEventWatcher.selectEventWatcher = function(evt) {
931 cvox.ChromeVoxEventWatcher.addEvent(evt);
936 * Watches for DOM subtree modified events.
938 * @param {Event} evt The event to add to the queue.
939 * @return {boolean} True if the default action should be performed.
941 cvox.ChromeVoxEventWatcher.subtreeModifiedEventWatcher = function(evt) {
942 if (!evt || !evt.target) {
945 cvox.ChromeVoxEventWatcher.addEvent(evt);
950 * Listens for WebKit visibility change events.
952 cvox.ChromeVoxEventWatcher.visibilityChangeWatcher = function() {
953 cvox.ChromeVoxEventWatcher.initialVisibility = !document.webkitHidden;
954 if (document.webkitHidden) {
955 cvox.ChromeVox.navigationManager.stopReading(true);
960 * Gets the initial visibility of the page.
961 * @return {boolean} True if the page is visible and this is the first request
962 * for visibility state.
964 cvox.ChromeVoxEventWatcher.getInitialVisibility = function() {
965 var ret = cvox.ChromeVoxEventWatcher.initialVisibility;
966 cvox.ChromeVoxEventWatcher.initialVisibility = false;
971 * Speaks the text of one live region.
972 * @param {boolean} assertive True if it's an assertive live region.
973 * @param {Array.<cvox.NavDescription>} messages An array of navDescriptions
974 * representing the description of the live region changes.
977 cvox.ChromeVoxEventWatcher.speakLiveRegion_ = function(
978 assertive, messages) {
979 var queueMode = cvox.ChromeVoxEventWatcher.queueMode_();
980 var descSpeaker = new cvox.NavigationSpeaker();
981 descSpeaker.speakDescriptionArray(messages, queueMode, null);
985 * Handles DOM subtree modified events passed to it from the events queue.
986 * If the change involves an ARIA live region, then speak it.
988 * @param {Event} evt The event to handle.
990 cvox.ChromeVoxEventWatcher.subtreeModifiedHandler = function(evt) {
991 // Subtree modified events can happen in bursts. If several events happen at
992 // the same time, trying to process all of them will slow ChromeVox to
993 // a crawl and make the page itself unresponsive (ie, Google+).
994 // Before processing subtree modified events, make sure that it is not part of
995 // a large burst of events.
996 // TODO (clchen): Revisit this after the DOM mutation events are
997 // available in Chrome.
998 var currentTime = new Date().getTime();
1000 if ((cvox.ChromeVoxEventWatcher.lastSubtreeModifiedEventBurstTime_ +
1001 cvox.ChromeVoxEventWatcher.SUBTREE_MODIFIED_BURST_DURATION_) >
1003 cvox.ChromeVoxEventWatcher.subtreeModifiedEventsCount_++;
1004 if (cvox.ChromeVoxEventWatcher.subtreeModifiedEventsCount_ >
1005 cvox.ChromeVoxEventWatcher.SUBTREE_MODIFIED_BURST_COUNT_LIMIT_) {
1009 cvox.ChromeVoxEventWatcher.lastSubtreeModifiedEventBurstTime_ = currentTime;
1010 cvox.ChromeVoxEventWatcher.subtreeModifiedEventsCount_ = 1;
1013 if (!evt || !evt.target) {
1016 var target = /** @type {Element} */ (evt.target);
1017 var regions = cvox.AriaUtil.getLiveRegions(target);
1018 for (var i = 0; (i < regions.length) &&
1019 (i < cvox.ChromeVoxEventWatcher.MAX_LIVE_REGIONS_); i++) {
1020 cvox.LiveRegionsDeprecated.updateLiveRegion(
1021 regions[i], cvox.ChromeVoxEventWatcher.queueMode_(), false);
1026 * Sets up the text handler.
1027 * @return {boolean} True if an editable text control has focus.
1029 cvox.ChromeVoxEventWatcher.setUpTextHandler = function() {
1030 var currentFocus = document.activeElement;
1032 currentFocus.hasAttribute &&
1033 currentFocus.getAttribute('aria-hidden') == 'true' &&
1034 currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1035 currentFocus = null;
1038 if (currentFocus != cvox.ChromeVoxEventWatcher.currentTextControl) {
1039 if (cvox.ChromeVoxEventWatcher.currentTextControl) {
1040 cvox.ChromeVoxEventWatcher.currentTextControl.removeEventListener(
1041 'input', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
1042 cvox.ChromeVoxEventWatcher.currentTextControl.removeEventListener(
1043 'click', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
1044 if (cvox.ChromeVoxEventWatcher.textMutationObserver_) {
1045 cvox.ChromeVoxEventWatcher.textMutationObserver_.disconnect();
1046 cvox.ChromeVoxEventWatcher.textMutationObserver_ = null;
1049 cvox.ChromeVoxEventWatcher.currentTextControl = null;
1050 if (cvox.ChromeVoxEventWatcher.currentTextHandler) {
1051 cvox.ChromeVoxEventWatcher.currentTextHandler.teardown();
1052 cvox.ChromeVoxEventWatcher.currentTextHandler = null;
1054 if (currentFocus == null) {
1057 if (currentFocus.constructor == HTMLInputElement &&
1058 cvox.DomUtil.isInputTypeText(currentFocus) &&
1059 cvox.ChromeVoxEventWatcher.shouldEchoKeys) {
1060 cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus;
1061 cvox.ChromeVoxEventWatcher.currentTextHandler =
1062 new cvox.ChromeVoxEditableHTMLInput(currentFocus, cvox.ChromeVox.tts);
1063 } else if ((currentFocus.constructor == HTMLTextAreaElement) &&
1064 cvox.ChromeVoxEventWatcher.shouldEchoKeys) {
1065 cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus;
1066 cvox.ChromeVoxEventWatcher.currentTextHandler =
1067 new cvox.ChromeVoxEditableTextArea(currentFocus, cvox.ChromeVox.tts);
1068 } else if (currentFocus.isContentEditable ||
1069 currentFocus.getAttribute('role') == 'textbox') {
1070 cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus;
1071 cvox.ChromeVoxEventWatcher.currentTextHandler =
1072 new cvox.ChromeVoxEditableContentEditable(currentFocus,
1073 cvox.ChromeVox.tts);
1076 if (cvox.ChromeVoxEventWatcher.currentTextControl) {
1077 cvox.ChromeVoxEventWatcher.currentTextControl.addEventListener(
1078 'input', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
1079 cvox.ChromeVoxEventWatcher.currentTextControl.addEventListener(
1080 'click', cvox.ChromeVoxEventWatcher.changeEventWatcher, false);
1081 if (window.WebKitMutationObserver) {
1082 cvox.ChromeVoxEventWatcher.textMutationObserver_ =
1083 new window.WebKitMutationObserver(
1084 cvox.ChromeVoxEventWatcher.onTextMutation);
1085 cvox.ChromeVoxEventWatcher.textMutationObserver_.observe(
1086 cvox.ChromeVoxEventWatcher.currentTextControl,
1087 /** @type {!MutationObserverInit} */ ({
1091 attributeOldValue: false,
1092 characterDataOldValue: false
1095 if (!cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
1096 cvox.ChromeVox.navigationManager.updateSel(
1097 cvox.CursorSelection.fromNode(
1098 cvox.ChromeVoxEventWatcher.currentTextControl));
1102 return (null != cvox.ChromeVoxEventWatcher.currentTextHandler);
1107 * Speaks updates to editable text controls as needed.
1109 * @param {boolean} isKeypress Was this change triggered by a keypress?
1110 * @return {boolean} True if an editable text control has focus.
1112 cvox.ChromeVoxEventWatcher.handleTextChanged = function(isKeypress) {
1113 if (cvox.ChromeVoxEventWatcher.currentTextHandler) {
1114 var handler = cvox.ChromeVoxEventWatcher.currentTextHandler;
1115 var shouldFlush = false;
1116 if (isKeypress && cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance) {
1118 cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
1120 handler.update(shouldFlush);
1121 cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
1128 * Called when an editable text control has focus, because many changes
1129 * to a text box don't ever generate events - e.g. if the page's javascript
1130 * changes the contents of the text box after some delay, or if it's
1131 * contentEditable or a generic div with role="textbox".
1133 cvox.ChromeVoxEventWatcher.onTextMutation = function() {
1134 if (cvox.ChromeVoxEventWatcher.currentTextHandler) {
1135 window.setTimeout(function() {
1136 cvox.ChromeVoxEventWatcher.handleTextChanged(false);
1137 }, cvox.ChromeVoxEventWatcher.MAX_WAIT_TIME_MS_);
1142 * Speaks updates to other form controls as needed.
1143 * @param {Element} control The target control.
1145 cvox.ChromeVoxEventWatcher.handleControlChanged = function(control) {
1146 var newValue = cvox.DomUtil.getControlValueAndStateString(control);
1147 var parentControl = cvox.DomUtil.getSurroundingControl(control);
1148 var announceChange = false;
1150 if (control != cvox.ChromeVoxEventWatcher.lastFocusedNode &&
1151 (parentControl == null ||
1152 parentControl != cvox.ChromeVoxEventWatcher.lastFocusedNode)) {
1153 cvox.ChromeVoxEventWatcher.setLastFocusedNode_(control);
1154 } else if (newValue == cvox.ChromeVoxEventWatcher.lastFocusedNodeValue) {
1158 cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = newValue;
1159 if (cvox.DomPredicates.checkboxPredicate([control]) ||
1160 cvox.DomPredicates.radioPredicate([control])) {
1161 // Always announce changes to checkboxes and radio buttons.
1162 announceChange = true;
1163 // Play earcons for checkboxes and radio buttons
1164 if (control.checked) {
1165 cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.CHECK_ON);
1167 cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.CHECK_OFF);
1171 if (control.tagName == 'SELECT') {
1172 announceChange = true;
1175 if (control.tagName == 'INPUT') {
1176 switch (control.type) {
1179 case 'datetime-local':
1181 announceChange = true;
1188 // Always announce changes for anything with an ARIA role.
1189 if (control.hasAttribute && control.hasAttribute('role')) {
1190 announceChange = true;
1193 if ((parentControl &&
1194 parentControl != control &&
1195 document.activeElement == control)) {
1196 // If focus has been set on a child of the parent control, we need to
1197 // sync to that node so that ChromeVox navigation will be in sync with
1198 // focus navigation.
1199 cvox.ApiImplementation.syncToNode(
1201 cvox.ChromeVoxEventWatcher.queueMode_());
1202 announceChange = false;
1203 } else if (cvox.AriaUtil.getActiveDescendant(control)) {
1204 cvox.ChromeVox.navigationManager.updateSelToArbitraryNode(
1205 cvox.AriaUtil.getActiveDescendant(control),
1208 announceChange = true;
1211 if (announceChange && !cvox.ChromeVoxEventSuspender.areEventsSuspended()) {
1212 cvox.ChromeVox.tts.speak(newValue,
1213 cvox.ChromeVoxEventWatcher.queueMode_(),
1215 cvox.NavBraille.fromText(newValue).write();
1220 * Handle actions on form controls triggered by key presses.
1221 * @param {Object} evt The event.
1222 * @return {boolean} True if this key event was handled.
1224 cvox.ChromeVoxEventWatcher.handleControlAction = function(evt) {
1225 // Ignore the control action if ChromeVox is not active.
1226 if (!cvox.ChromeVox.isActive) {
1229 var control = evt.target;
1231 if (control.tagName == 'SELECT' && (control.size <= 1) &&
1232 (evt.keyCode == 13 || evt.keyCode == 32)) { // Enter or Space
1233 // TODO (dmazzoni, clchen): Remove this workaround once accessibility
1234 // APIs make browser based popups accessible.
1236 // Do nothing, but eat this keystroke when the SELECT control
1237 // has a dropdown style since if we don't, it will generate
1238 // a browser popup menu which is not accessible.
1239 // List style SELECT controls are fine and don't need this workaround.
1240 evt.preventDefault();
1241 evt.stopPropagation();
1245 if (control.tagName == 'INPUT' && control.type == 'range') {
1246 var value = parseFloat(control.value);
1248 if (control.step && control.step > 0.0) {
1249 step = control.step;
1250 } else if (control.min && control.max) {
1251 var range = (control.max - control.min);
1252 if (range > 2 && range < 31) {
1255 step = (control.max - control.min) / 10;
1261 if (evt.keyCode == 37 || evt.keyCode == 38) { // left or up
1263 } else if (evt.keyCode == 39 || evt.keyCode == 40) { // right or down
1267 if (control.max && value > control.max) {
1268 value = control.max;
1270 if (control.min && value < control.min) {
1271 value = control.min;
1274 control.value = value;
1280 * When an element receives focus, see if we've entered or left a dialog
1281 * and return a string describing the event.
1283 * @param {Element} target The element that just received focus.
1284 * @return {boolean} True if an announcement was spoken.
1286 cvox.ChromeVoxEventWatcher.handleDialogFocus = function(target) {
1287 var dialog = target;
1290 if (dialog.hasAttribute) {
1291 role = dialog.getAttribute('role');
1292 if (role == 'dialog' || role == 'alertdialog') {
1296 dialog = dialog.parentElement;
1299 if (dialog == cvox.ChromeVox.navigationManager.currentDialog) {
1303 if (cvox.ChromeVox.navigationManager.currentDialog && !dialog) {
1304 if (!cvox.DomUtil.isDescendantOfNode(
1305 document.activeElement,
1306 cvox.ChromeVox.navigationManager.currentDialog)) {
1307 cvox.ChromeVox.navigationManager.currentDialog = null;
1309 cvox.ChromeVox.tts.speak(
1310 cvox.ChromeVox.msgs.getMsg('exiting_dialog'),
1311 cvox.ChromeVoxEventWatcher.queueMode_(),
1312 cvox.AbstractTts.PERSONALITY_ANNOTATION);
1317 cvox.ChromeVox.navigationManager.currentDialog = dialog;
1318 cvox.ChromeVox.tts.speak(
1319 cvox.ChromeVox.msgs.getMsg('entering_dialog'),
1320 cvox.ChromeVoxEventWatcher.queueMode_(),
1321 cvox.AbstractTts.PERSONALITY_ANNOTATION);
1322 if (role == 'alertdialog') {
1323 var dialogDescArray =
1324 cvox.DescriptionUtil.getFullDescriptionsFromChildren(null, dialog);
1325 var descSpeaker = new cvox.NavigationSpeaker();
1326 descSpeaker.speakDescriptionArray(dialogDescArray,
1327 cvox.AbstractTts.QUEUE_MODE_QUEUE,
1337 * Returns true if we should wait to process events.
1338 * @param {number} lastFocusTimestamp The timestamp of the last focus event.
1339 * @param {number} firstTimestamp The timestamp of the first event.
1340 * @param {number} currentTime The current timestamp.
1341 * @return {boolean} True if we should wait to process events.
1343 cvox.ChromeVoxEventWatcherUtil.shouldWaitToProcess = function(
1344 lastFocusTimestamp, firstTimestamp, currentTime) {
1345 var timeSinceFocusEvent = currentTime - lastFocusTimestamp;
1346 var timeSinceFirstEvent = currentTime - firstTimestamp;
1347 return timeSinceFocusEvent < cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_ &&
1348 timeSinceFirstEvent < cvox.ChromeVoxEventWatcher.MAX_WAIT_TIME_MS_;
1353 * Returns the queue mode to use for the next utterance spoken as
1354 * a result of an event or navigation. The first utterance that's spoken
1355 * after an explicit user action like a key press will flush, and
1356 * subsequent events will return a category flush.
1357 * @return {number} Either QUEUE_MODE_FLUSH or QUEUE_MODE_QUEUE.
1360 cvox.ChromeVoxEventWatcher.queueMode_ = function() {
1361 if (cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance) {
1362 cvox.ChromeVoxEventWatcher.shouldFlushNextUtterance = false;
1363 return cvox.AbstractTts.QUEUE_MODE_FLUSH;
1365 return cvox.AbstractTts.QUEUE_MODE_CATEGORY_FLUSH;
1370 * Processes the events queue.
1374 cvox.ChromeVoxEventWatcher.processQueue_ = function() {
1375 // Return now if there are no events in the queue.
1376 if (cvox.ChromeVoxEventWatcher.events_.length == 0) {
1380 // Look for the most recent focus event and delete any preceding event
1381 // that applied to whatever was focused previously.
1382 var events = cvox.ChromeVoxEventWatcher.events_;
1383 var lastFocusIndex = -1;
1384 var lastFocusTimestamp = 0;
1387 for (i = 0; evt = events[i]; i++) {
1388 if (evt.type == 'focus') {
1390 lastFocusTimestamp = evt.timeStamp;
1393 cvox.ChromeVoxEventWatcher.events_ = [];
1394 for (i = 0; evt = events[i]; i++) {
1395 var prevEvt = events[i - 1] || {};
1396 if ((i >= lastFocusIndex || evt.type == 'LiveRegion' ||
1397 evt.type == 'DOMSubtreeModified') &&
1398 (prevEvt.type != 'focus' || evt.type != 'change')) {
1399 cvox.ChromeVoxEventWatcher.events_.push(evt);
1403 cvox.ChromeVoxEventWatcher.events_.sort(function(a, b) {
1404 if (b.type != 'LiveRegion' && a.type == 'LiveRegion') {
1407 if (b.type != 'DOMSubtreeModified' && a.type == 'DOMSubtreeModified') {
1413 // If the most recent focus event was very recent, wait for things to
1414 // settle down before processing events, unless the max wait time has
1416 var currentTime = new Date().getTime();
1417 if (lastFocusIndex >= 0 &&
1418 cvox.ChromeVoxEventWatcherUtil.shouldWaitToProcess(
1420 cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime,
1422 window.setTimeout(cvox.ChromeVoxEventWatcher.processQueue_,
1423 cvox.ChromeVoxEventWatcher.WAIT_TIME_MS_);
1427 // Process the remaining events in the queue, in order.
1428 for (i = 0; evt = cvox.ChromeVoxEventWatcher.events_[i]; i++) {
1429 cvox.ChromeVoxEventWatcher.handleEvent_(evt);
1431 cvox.ChromeVoxEventWatcher.events_ = new Array();
1432 cvox.ChromeVoxEventWatcher.firstUnprocessedEventTime = -1;
1433 cvox.ChromeVoxEventWatcher.queueProcessingScheduled_ = false;
1434 cvox.ChromeVoxEventWatcher.maybeCallReadyCallbacks_();
1438 * Handle events from the queue by routing them to their respective handlers.
1441 * @param {Event} evt The event to be handled.
1443 cvox.ChromeVoxEventWatcher.handleEvent_ = function(evt) {
1447 cvox.ChromeVoxEventWatcher.setUpTextHandler();
1448 if (cvox.ChromeVoxEventWatcher.currentTextControl) {
1449 cvox.ChromeVoxEventWatcher.handleTextChanged(true);
1451 var editableText = /** @type {cvox.ChromeVoxEditableTextBase} */
1452 (cvox.ChromeVoxEventWatcher.currentTextHandler);
1453 if (editableText && editableText.lastChangeDescribed) {
1457 // We're either not on a text control, or we are on a text control but no
1458 // text change was described. Let's try describing the state instead.
1459 cvox.ChromeVoxEventWatcher.handleControlChanged(document.activeElement);
1462 // Some controls change only after key up.
1463 cvox.ChromeVoxEventWatcher.handleControlChanged(document.activeElement);
1466 cvox.ChromeVoxEventWatcher.setUpTextHandler();
1469 cvox.ApiImplementation.syncToNode(/** @type {Node} */(evt.target), true);
1472 cvox.ChromeVoxEventWatcher.focusHandler(evt);
1475 cvox.ChromeVoxEventWatcher.setUpTextHandler();
1478 cvox.ChromeVoxEventWatcher.changeHandler(evt);
1481 cvox.ChromeVoxEventWatcher.setUpTextHandler();
1484 cvox.ChromeVoxEventWatcher.speakLiveRegion_(
1485 evt.assertive, evt.navDescriptions);
1487 case 'DOMSubtreeModified':
1488 cvox.ChromeVoxEventWatcher.subtreeModifiedHandler(evt);
1495 * Sets up the time handler.
1496 * @return {boolean} True if a time control has focus.
1499 cvox.ChromeVoxEventWatcher.setUpTimeHandler_ = function() {
1500 var currentFocus = document.activeElement;
1502 currentFocus.hasAttribute &&
1503 currentFocus.getAttribute('aria-hidden') == 'true' &&
1504 currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1505 currentFocus = null;
1507 if (currentFocus.constructor == HTMLInputElement &&
1508 currentFocus.type && (currentFocus.type == 'time')) {
1509 cvox.ChromeVoxEventWatcher.currentTimeHandler =
1510 new cvox.ChromeVoxHTMLTimeWidget(currentFocus, cvox.ChromeVox.tts);
1512 cvox.ChromeVoxEventWatcher.currentTimeHandler = null;
1514 return (null != cvox.ChromeVoxEventWatcher.currentTimeHandler);
1519 * Sets up the media (video/audio) handler.
1520 * @return {boolean} True if a media control has focus.
1523 cvox.ChromeVoxEventWatcher.setUpMediaHandler_ = function() {
1524 var currentFocus = document.activeElement;
1526 currentFocus.hasAttribute &&
1527 currentFocus.getAttribute('aria-hidden') == 'true' &&
1528 currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1529 currentFocus = null;
1531 if ((currentFocus.constructor == HTMLVideoElement) ||
1532 (currentFocus.constructor == HTMLAudioElement)) {
1533 cvox.ChromeVoxEventWatcher.currentMediaHandler =
1534 new cvox.ChromeVoxHTMLMediaWidget(currentFocus, cvox.ChromeVox.tts);
1536 cvox.ChromeVoxEventWatcher.currentMediaHandler = null;
1538 return (null != cvox.ChromeVoxEventWatcher.currentMediaHandler);
1542 * Sets up the date handler.
1543 * @return {boolean} True if a date control has focus.
1546 cvox.ChromeVoxEventWatcher.setUpDateHandler_ = function() {
1547 var currentFocus = document.activeElement;
1549 currentFocus.hasAttribute &&
1550 currentFocus.getAttribute('aria-hidden') == 'true' &&
1551 currentFocus.getAttribute('chromevoxignoreariahidden') != 'true') {
1552 currentFocus = null;
1554 if (currentFocus.constructor == HTMLInputElement &&
1555 currentFocus.type &&
1556 ((currentFocus.type == 'date') ||
1557 (currentFocus.type == 'month') ||
1558 (currentFocus.type == 'week'))) {
1559 cvox.ChromeVoxEventWatcher.currentDateHandler =
1560 new cvox.ChromeVoxHTMLDateWidget(currentFocus, cvox.ChromeVox.tts);
1562 cvox.ChromeVoxEventWatcher.currentDateHandler = null;
1564 return (null != cvox.ChromeVoxEventWatcher.currentDateHandler);