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