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