OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2011 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 goog.provide('cvox.ChromeVoxEventWatcher'); |
| 10 |
| 11 goog.require('cvox.ChromeVox'); |
| 12 goog.require('cvox.ChromeVoxEditableTextBase'); |
| 13 goog.require('cvox.ChromeVoxKbHandler'); |
| 14 goog.require('cvox.ChromeVoxUserCommands'); |
| 15 goog.require('cvox.DomUtil'); |
| 16 |
| 17 /** |
| 18 * @constructor |
| 19 */ |
| 20 cvox.ChromeVoxEventWatcher = function() { |
| 21 }; |
| 22 |
| 23 /** |
| 24 * @type {Object} |
| 25 */ |
| 26 cvox.ChromeVoxEventWatcher.lastFocusedNode = null; |
| 27 |
| 28 /** |
| 29 * @type {string?} |
| 30 */ |
| 31 cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = null; |
| 32 |
| 33 /** |
| 34 * @type {Object} |
| 35 */ |
| 36 cvox.ChromeVoxEventWatcher.eventToEat = null; |
| 37 |
| 38 /** |
| 39 * @type {Element} |
| 40 */ |
| 41 cvox.ChromeVoxEventWatcher.currentTextControl = null; |
| 42 |
| 43 /** |
| 44 * @type {cvox.ChromeVoxEditableTextBase} |
| 45 */ |
| 46 cvox.ChromeVoxEventWatcher.currentTextHandler = null; |
| 47 |
| 48 /** |
| 49 * @type {Object} |
| 50 */ |
| 51 cvox.ChromeVoxEventWatcher.previousTextHandlerState = null; |
| 52 |
| 53 /** |
| 54 * The last timestamp for the last keypress; that helps us separate |
| 55 * user-triggered events from other events. |
| 56 * @type {number} |
| 57 */ |
| 58 cvox.ChromeVoxEventWatcher.lastKeypressTime = 0; |
| 59 |
| 60 /** |
| 61 * The delay before the timer function is first called to check on a |
| 62 * focused text control, to see if it's been modified without an event |
| 63 * being generated. |
| 64 * @const |
| 65 * @type {number} |
| 66 */ |
| 67 cvox.ChromeVoxEventWatcher.TEXT_TIMER_INITIAL_DELAY_MS = 10; |
| 68 |
| 69 /** |
| 70 * The delay between subsequent calls to the timer function to check |
| 71 * focused text controls. |
| 72 * @const |
| 73 * @type {number} |
| 74 */ |
| 75 cvox.ChromeVoxEventWatcher.TEXT_TIMER_DELAY_MS = 250; |
| 76 |
| 77 /** |
| 78 * Add all of our event listeners to the document. |
| 79 */ |
| 80 cvox.ChromeVoxEventWatcher.addEventListeners = function() { |
| 81 document.addEventListener( |
| 82 'keypress', cvox.ChromeVoxEventWatcher.keyPressEventWatcher, true); |
| 83 document.addEventListener( |
| 84 'keydown', cvox.ChromeVoxEventWatcher.keyDownEventWatcher, true); |
| 85 document.addEventListener( |
| 86 'keyup', cvox.ChromeVoxEventWatcher.keyUpEventWatcher, true); |
| 87 document.addEventListener( |
| 88 'focus', cvox.ChromeVoxEventWatcher.focusEventWatcher, true); |
| 89 document.addEventListener( |
| 90 'blur', cvox.ChromeVoxEventWatcher.blurEventWatcher, true); |
| 91 document.addEventListener( |
| 92 'change', cvox.ChromeVoxEventWatcher.changeEventWatcher, true); |
| 93 document.addEventListener( |
| 94 'select', cvox.ChromeVoxEventWatcher.selectEventWatcher, true); |
| 95 }; |
| 96 |
| 97 /** |
| 98 * Return the last focused node. |
| 99 * @return {Object} The last node that was focused. |
| 100 */ |
| 101 cvox.ChromeVoxEventWatcher.getLastFocusedNode = function() { |
| 102 return cvox.ChromeVoxEventWatcher.lastFocusedNode; |
| 103 }; |
| 104 |
| 105 /** |
| 106 * Handles focus events. |
| 107 * |
| 108 * @param {Event} evt The focus event to process. |
| 109 * @return {boolean} True if the default action should be performed. |
| 110 */ |
| 111 cvox.ChromeVoxEventWatcher.focusEventWatcher = function(evt) { |
| 112 cvox.ChromeVoxEventWatcher.lastFocusedNode = evt.target; |
| 113 if (evt.target) { |
| 114 if (cvox.DomUtil.isControl(evt.target)) { |
| 115 cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = |
| 116 cvox.ChromeVoxEventWatcher.getControlValueAndStateString( |
| 117 /** @type {Element} */(evt.target)); |
| 118 cvox.ChromeVox.tts.speak(cvox.ChromeVoxEventWatcher.lastFocusedNodeValue, |
| 119 0, null); |
| 120 } |
| 121 if (evt.target.tagName == 'A' && |
| 122 cvox.ChromeVoxUserCommands.silenceLevel == 0) { |
| 123 cvox.ChromeVox.tts.speak(cvox.DomUtil.getText(evt.target), 0, null); |
| 124 } |
| 125 cvox.ChromeVox.navigationManager.syncToNode(evt.target); |
| 126 } else { |
| 127 cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = null; |
| 128 } |
| 129 cvox.ChromeVoxEventWatcher.handleTextChanged(false); |
| 130 return true; |
| 131 }; |
| 132 |
| 133 /** |
| 134 * Handles blur events. |
| 135 * |
| 136 * @param {Object} evt The blur event to process. |
| 137 * @return {boolean} True if the default action should be performed. |
| 138 */ |
| 139 cvox.ChromeVoxEventWatcher.blurEventWatcher = function(evt) { |
| 140 cvox.ChromeVoxEventWatcher.lastFocusedNode = null; |
| 141 cvox.ChromeVoxEventWatcher.handleTextChanged(false); |
| 142 return true; |
| 143 }; |
| 144 |
| 145 /** |
| 146 * Handles key down events. |
| 147 * |
| 148 * @param {Object} evt The event to process. |
| 149 * @return {boolean} True if the default action should be performed. |
| 150 */ |
| 151 cvox.ChromeVoxEventWatcher.keyDownEventWatcher = function(evt) { |
| 152 if (cvox.ChromeVoxEventWatcher.currentTextHandler) { |
| 153 cvox.ChromeVoxEventWatcher.previousTextHandlerState = |
| 154 cvox.ChromeVoxEventWatcher.currentTextHandler.saveState(); |
| 155 } |
| 156 cvox.ChromeVoxEventWatcher.lastKeypressTime = new Date().getTime(); |
| 157 |
| 158 cvox.ChromeVoxEventWatcher.eventToEat = null; |
| 159 if (!cvox.ChromeVoxKbHandler.basicKeyDownActionsListener(evt) || |
| 160 cvox.ChromeVoxEventWatcher.handleControlAction(evt)) { |
| 161 // Swallow the event immediately to prevent the arrow keys |
| 162 // from driving controls on the web page. |
| 163 evt.preventDefault(); |
| 164 evt.stopPropagation(); |
| 165 // Also mark this as something to be swallowed when the followup |
| 166 // keypress/keyup counterparts to this event show up later. |
| 167 cvox.ChromeVoxEventWatcher.eventToEat = evt; |
| 168 return false; |
| 169 } |
| 170 cvox.ChromeVoxEventWatcher.handleTextChanged(true); |
| 171 setTimeout(function() { |
| 172 cvox.ChromeVoxEventWatcher.handleControlChanged(evt.target); |
| 173 }, 0); |
| 174 return true; |
| 175 }; |
| 176 |
| 177 /** |
| 178 * Handles key press events. |
| 179 * |
| 180 * @param {Object} evt The event to process. |
| 181 * @return {boolean} True if the default action should be performed. |
| 182 */ |
| 183 cvox.ChromeVoxEventWatcher.keyPressEventWatcher = function(evt) { |
| 184 cvox.ChromeVoxEventWatcher.handleTextChanged(false); |
| 185 |
| 186 if (cvox.ChromeVoxEventWatcher.eventToEat && |
| 187 evt.keyCode == cvox.ChromeVoxEventWatcher.eventToEat.keyCode) { |
| 188 evt.preventDefault(); |
| 189 evt.stopPropagation(); |
| 190 return false; |
| 191 } |
| 192 return true; |
| 193 }; |
| 194 |
| 195 /** |
| 196 * Handles key up events. |
| 197 * |
| 198 * @param {Object} evt The event to process. |
| 199 * @return {boolean} True if the default action should be performed. |
| 200 */ |
| 201 cvox.ChromeVoxEventWatcher.keyUpEventWatcher = function(evt) { |
| 202 if (cvox.ChromeVoxEventWatcher.eventToEat && |
| 203 evt.keyCode == cvox.ChromeVoxEventWatcher.eventToEat.keyCode) { |
| 204 evt.stopPropagation(); |
| 205 evt.preventDefault(); |
| 206 return false; |
| 207 } |
| 208 return true; |
| 209 }; |
| 210 |
| 211 /** |
| 212 * Handles change events. |
| 213 * |
| 214 * @param {Object} evt The event to process. |
| 215 * @return {boolean} True if the default action should be performed. |
| 216 */ |
| 217 cvox.ChromeVoxEventWatcher.changeEventWatcher = function(evt) { |
| 218 if (cvox.ChromeVoxEventWatcher.handleTextChanged(false)) { |
| 219 return true; |
| 220 } |
| 221 return true; |
| 222 }; |
| 223 |
| 224 /** |
| 225 * Handles select events. |
| 226 * |
| 227 * @param {Object} evt The event to process. |
| 228 * @return {boolean} True if the default action should be performed. |
| 229 */ |
| 230 cvox.ChromeVoxEventWatcher.selectEventWatcher = function(evt) { |
| 231 if (cvox.ChromeVoxEventWatcher.handleTextChanged(false)) { |
| 232 return true; |
| 233 } |
| 234 return true; |
| 235 }; |
| 236 |
| 237 /** |
| 238 * Speaks updates to editable text controls as needed. |
| 239 * @param {boolean} isKeypress Was this change triggered by a keypress? |
| 240 * @return {boolean} True if an editable text control has focus. |
| 241 */ |
| 242 cvox.ChromeVoxEventWatcher.handleTextChanged = function(isKeypress) { |
| 243 var currentFocus = document.activeElement; |
| 244 |
| 245 if (currentFocus != cvox.ChromeVoxEventWatcher.currentTextControl) { |
| 246 if (cvox.ChromeVoxEventWatcher.currentTextControl) { |
| 247 cvox.ChromeVoxEventWatcher.currentTextControl.removeEventListener( |
| 248 'input', cvox.ChromeVoxEventWatcher.changeEventWatcher, false); |
| 249 cvox.ChromeVoxEventWatcher.currentTextControl.removeEventListener( |
| 250 'click', cvox.ChromeVoxEventWatcher.changeEventWatcher, false); |
| 251 } |
| 252 cvox.ChromeVoxEventWatcher.currentTextControl = null; |
| 253 cvox.ChromeVoxEventWatcher.currentTextHandler = null; |
| 254 cvox.ChromeVoxEventWatcher.previousTextHandlerState = null; |
| 255 |
| 256 if (currentFocus == null) { |
| 257 return false; |
| 258 } |
| 259 |
| 260 if (currentFocus.constructor == HTMLInputElement && |
| 261 cvox.DomUtil.isInputTypeText(currentFocus)) { |
| 262 cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus; |
| 263 cvox.ChromeVoxEventWatcher.currentTextHandler = |
| 264 new cvox.ChromeVoxEditableHTMLInput(currentFocus, cvox.ChromeVox.tts); |
| 265 } else if (currentFocus.constructor == HTMLTextAreaElement) { |
| 266 cvox.ChromeVoxEventWatcher.currentTextControl = currentFocus; |
| 267 cvox.ChromeVoxEventWatcher.currentTextHandler = |
| 268 new cvox.ChromeVoxEditableTextArea(currentFocus, cvox.ChromeVox.tts); |
| 269 } |
| 270 |
| 271 if (cvox.ChromeVoxEventWatcher.currentTextControl) { |
| 272 cvox.ChromeVoxEventWatcher.currentTextControl.addEventListener( |
| 273 'input', cvox.ChromeVoxEventWatcher.changeEventWatcher, false); |
| 274 cvox.ChromeVoxEventWatcher.currentTextControl.addEventListener( |
| 275 'click', cvox.ChromeVoxEventWatcher.changeEventWatcher, false); |
| 276 cvox.ChromeVoxEventWatcher.currentTextHandler.describe(); |
| 277 window.setTimeout(cvox.ChromeVoxEventWatcher.textTimer, |
| 278 cvox.ChromeVoxEventWatcher.TEXT_TIMER_INITIAL_DELAY_MS); |
| 279 cvox.ChromeVox.navigationManager.syncToNode( |
| 280 cvox.ChromeVoxEventWatcher.currentTextControl); |
| 281 } |
| 282 |
| 283 return (null != cvox.ChromeVoxEventWatcher.currentTextHandler); |
| 284 } |
| 285 |
| 286 if (cvox.ChromeVoxEventWatcher.currentTextHandler) { |
| 287 var handler = cvox.ChromeVoxEventWatcher.currentTextHandler; |
| 288 window.setTimeout(function() { |
| 289 // If this update was not triggered by an explicit user keypress, |
| 290 // and we already started speaking an update to this text control |
| 291 // very recently (less than 50 ms ago), restore the control to its |
| 292 // previous state and then speak the new update (interrupting any |
| 293 // ongoing speech). That way, if the user presses a key and the |
| 294 // page's javascript causes a few more characters to be inserted, |
| 295 // we'll speak it as one big update. |
| 296 var now = new Date().getTime(); |
| 297 if (!isKeypress && |
| 298 handler.needsUpdate() && |
| 299 cvox.ChromeVoxEventWatcher.previousTextHandlerState && |
| 300 now - cvox.ChromeVoxEventWatcher.lastKeypressTime < 50) { |
| 301 handler.restoreState( |
| 302 cvox.ChromeVoxEventWatcher.previousTextHandlerState); |
| 303 } |
| 304 |
| 305 handler.update(); |
| 306 }, 0); |
| 307 return true; |
| 308 } else { |
| 309 } |
| 310 |
| 311 return false; |
| 312 }; |
| 313 |
| 314 /** |
| 315 * Called repeatedly while a text box has focus, because many changes |
| 316 * to a text box don't ever generate events - e.g. if the page's javascript |
| 317 * changes the contents of the text box after some delay. |
| 318 */ |
| 319 cvox.ChromeVoxEventWatcher.textTimer = function() { |
| 320 if (cvox.ChromeVoxEventWatcher.currentTextHandler && |
| 321 cvox.ChromeVoxEventWatcher.currentTextHandler.needsUpdate()) { |
| 322 cvox.ChromeVoxEventWatcher.handleTextChanged(false); |
| 323 } |
| 324 |
| 325 if (cvox.ChromeVoxEventWatcher.currentTextControl) { |
| 326 window.setTimeout(cvox.ChromeVoxEventWatcher.textTimer, |
| 327 cvox.ChromeVoxEventWatcher.TEXT_TIMER_DELAY_MS); |
| 328 } |
| 329 }; |
| 330 |
| 331 /** |
| 332 * Speaks updates to other form controls as needed. |
| 333 * @param {Element} control The target control. |
| 334 */ |
| 335 cvox.ChromeVoxEventWatcher.handleControlChanged = function(control) { |
| 336 var newValue = cvox.ChromeVoxEventWatcher.getControlValueAndStateString( |
| 337 control); |
| 338 |
| 339 if (control != cvox.ChromeVoxEventWatcher.lastFocusedNode) { |
| 340 cvox.ChromeVoxEventWatcher.lastFocusedNode = control; |
| 341 cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = newValue; |
| 342 return; |
| 343 } |
| 344 |
| 345 if (newValue == cvox.ChromeVoxEventWatcher.lastFocusedNodeValue) { |
| 346 return; |
| 347 } |
| 348 |
| 349 cvox.ChromeVoxEventWatcher.lastFocusedNodeValue = newValue; |
| 350 |
| 351 var announceChange = false; |
| 352 |
| 353 if (control.tagName == 'SELECT') { |
| 354 announceChange = true; |
| 355 } |
| 356 |
| 357 if (control.tagName == 'INPUT') { |
| 358 switch (control.type) { |
| 359 case 'checkbox': |
| 360 case 'color': |
| 361 case 'datetime': |
| 362 case 'datetime-local': |
| 363 case 'date': |
| 364 case 'month': |
| 365 case 'radio': |
| 366 case 'range': |
| 367 case 'week': |
| 368 announceChange = true; |
| 369 break; |
| 370 default: |
| 371 break; |
| 372 } |
| 373 } |
| 374 |
| 375 if (announceChange && cvox.ChromeVoxUserCommands.silenceLevel == 0) { |
| 376 cvox.ChromeVox.tts.speak(newValue, 0, null); |
| 377 } |
| 378 }; |
| 379 |
| 380 /** |
| 381 * Handle actions on form controls triggered by key presses. |
| 382 * @param {Object} evt The event. |
| 383 * @return {boolean} True if this key event was handled. |
| 384 */ |
| 385 cvox.ChromeVoxEventWatcher.handleControlAction = function(evt) { |
| 386 var control = evt.target; |
| 387 |
| 388 if (control.tagName == 'SELECT' && |
| 389 (evt.keyCode == 13 || evt.keyCode == 32)) { // Enter or Space |
| 390 evt.preventDefault(); |
| 391 evt.stopPropagation(); |
| 392 // Do nothing, but eat this keystroke |
| 393 return true; |
| 394 } |
| 395 |
| 396 if (control.tagName == 'INPUT' && control.type == 'range') { |
| 397 var value = parseFloat(control.value); |
| 398 var step; |
| 399 if (control.step && control.step > 0.0) { |
| 400 step = control.step; |
| 401 } else if (control.min && control.max) { |
| 402 var range = (control.max - control.min); |
| 403 if (range > 2 && range < 31) { |
| 404 step = 1; |
| 405 } else { |
| 406 step = (control.max - control.min) / 10; |
| 407 } |
| 408 } else { |
| 409 step = 1; |
| 410 } |
| 411 |
| 412 if (evt.keyCode == 37 || evt.keyCode == 38) { // left or up |
| 413 value -= step; |
| 414 } else if (evt.keyCode == 39 || evt.keyCode == 40) { // right or down |
| 415 value += step; |
| 416 } |
| 417 |
| 418 if (control.max && value > control.max) { |
| 419 value = control.max; |
| 420 } |
| 421 if (control.min && value < control.min) { |
| 422 value = control.min; |
| 423 } |
| 424 |
| 425 control.value = value; |
| 426 } |
| 427 |
| 428 return false; |
| 429 }; |
| 430 |
| 431 /** |
| 432 * Get a string representing a control's value and state. |
| 433 * @param {Element} control A control. |
| 434 * @return {string} A string representing a control's value and state. |
| 435 */ |
| 436 cvox.ChromeVoxEventWatcher.getControlValueAndStateString = function( |
| 437 control) { |
| 438 var controlValue = cvox.DomUtil.getValue(control); |
| 439 var controlState = cvox.DomUtil.getBasicNodeState(control); |
| 440 var controlTitle = cvox.DomUtil.getTitle(control); |
| 441 var controlLabel = cvox.DomUtil.getLabel(control, false); |
| 442 if ((controlTitle.length < 1) && (controlLabel.length < 1)) { |
| 443 controlLabel = cvox.DomUtil.getLabel(control, true); |
| 444 } |
| 445 return controlLabel + ' ' + controlTitle + ' ' + controlValue + ' ' + |
| 446 controlState; |
| 447 }; |
OLD | NEW |