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