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