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 Public APIs to enable web applications to communicate 7 * with ChromeVox. 8 */ 9 10if (typeof(goog) != 'undefined' && goog.provide) { 11 goog.provide('cvox.Api'); 12 goog.provide('cvox.Api.Math'); 13} 14 15if (typeof(goog) != 'undefined' && goog.require) { 16 goog.require('cvox.ApiImplementation'); 17} 18 19(function() { 20 /* 21 * Private data and methods. 22 */ 23 24 /** 25 * The name of the port between the content script and background page. 26 * @type {string} 27 * @const 28 */ 29 var PORT_NAME = 'cvox.Port'; 30 31 /** 32 * The name of the message between the page and content script that sets 33 * up the bidirectional port between them. 34 * @type {string} 35 * @const 36 */ 37 var PORT_SETUP_MSG = 'cvox.PortSetup'; 38 39 /** 40 * The message between content script and the page that indicates the 41 * connection to the background page has been lost. 42 * @type {string} 43 * @const 44 */ 45 var DISCONNECT_MSG = 'cvox.Disconnect'; 46 47 /** 48 * The channel between the page and content script. 49 * @type {MessageChannel} 50 */ 51 var channel_; 52 53 /** 54 * Tracks whether or not the ChromeVox API should be considered active. 55 * @type {boolean} 56 */ 57 var isActive_ = false; 58 59 /** 60 * The next id to use for async callbacks. 61 * @type {number} 62 */ 63 var nextCallbackId_ = 1; 64 65 /** 66 * Map from callback ID to callback function. 67 * @type {Object.<number, function(*)>} 68 */ 69 var callbackMap_ = {}; 70 71 /** 72 * Internal function to connect to the content script. 73 */ 74 function connect_() { 75 if (channel_) { 76 // If there is already an existing channel, close the existing ports. 77 channel_.port1.close(); 78 channel_.port2.close(); 79 channel_ = null; 80 } 81 82 channel_ = new MessageChannel(); 83 window.postMessage(PORT_SETUP_MSG, [channel_.port2], '*'); 84 channel_.port1.onmessage = function(event) { 85 if (event.data == DISCONNECT_MSG) { 86 channel_ = null; 87 } 88 try { 89 var message = JSON.parse(event.data); 90 if (message['id'] && callbackMap_[message['id']]) { 91 callbackMap_[message['id']](message); 92 delete callbackMap_[message['id']]; 93 } 94 } catch (e) { 95 } 96 }; 97 } 98 99 /** 100 * Internal function to send a message to the content script and 101 * call a callback with the response. 102 * @param {Object} message A serializable message. 103 * @param {function(*)} callback A callback that will be called 104 * with the response message. 105 */ 106 function callAsync_(message, callback) { 107 var id = nextCallbackId_; 108 nextCallbackId_++; 109 if (message['args'] === undefined) { 110 message['args'] = []; 111 } 112 message['args'] = [id].concat(message['args']); 113 callbackMap_[id] = callback; 114 channel_.port1.postMessage(JSON.stringify(message)); 115 } 116 117 /** 118 * Wraps callAsync_ for sending speak requests. 119 * @param {Object} message A serializable message. 120 * @param {Object=} properties Speech properties to use for this utterance. 121 * @private 122 */ 123 function callSpeakAsync_(message, properties) { 124 var callback = null; 125 /* Use the user supplied callback as callAsync_'s callback. */ 126 if (properties && properties['endCallback']) { 127 callback = properties['endCallback']; 128 } 129 callAsync_(message, callback); 130 }; 131 132 133 /* 134 * Public API. 135 */ 136 137 if (!window['cvox']) { 138 window['cvox'] = {}; 139 } 140 var cvox = window.cvox; 141 142 143 /** 144 * ApiImplementation - this is only visible if all the scripts are compiled 145 * together like in the Android case. Otherwise, implementation will remain 146 * null which means communication must happen over the bridge. 147 * 148 * @type {*} 149 */ 150 var implementation_ = null; 151 if (typeof(cvox.ApiImplementation) != 'undefined') { 152 implementation_ = cvox.ApiImplementation; 153 } 154 155 156 /** 157 * @constructor 158 */ 159 cvox.Api = function() { 160 }; 161 162 /** 163 * Internal-only function, only to be called by the content script. 164 * Enables the API and connects to the content script. 165 */ 166 cvox.Api.internalEnable = function() { 167 isActive_ = true; 168 if (!implementation_) { 169 connect_(); 170 } 171 var event = document.createEvent('UIEvents'); 172 event.initEvent('chromeVoxLoaded', true, false); 173 document.dispatchEvent(event); 174 }; 175 176 /** 177 * Internal-only function, only to be called by the content script. 178 * Disables the ChromeVox API. 179 */ 180 cvox.Api.internalDisable = function() { 181 isActive_ = false; 182 channel_ = null; 183 var event = document.createEvent('UIEvents'); 184 event.initEvent('chromeVoxUnloaded', true, false); 185 document.dispatchEvent(event); 186 }; 187 188 /** 189 * Returns true if ChromeVox is currently running. If the API is available 190 * in the JavaScript namespace but this method returns false, it means that 191 * the user has (temporarily) disabled ChromeVox. 192 * 193 * You can listen for the 'chromeVoxLoaded' event to be notified when 194 * ChromeVox is loaded. 195 * 196 * @return {boolean} True if ChromeVox is currently active. 197 */ 198 cvox.Api.isChromeVoxActive = function() { 199 if (implementation_) { 200 return isActive_; 201 } 202 return !!channel_; 203 }; 204 205 /** 206 * Speaks the given string using the specified queueMode and properties. 207 * 208 * @param {string} textString The string of text to be spoken. 209 * @param {number=} queueMode Valid modes are 0 for flush; 1 for queue. 210 * @param {Object=} properties Speech properties to use for this utterance. 211 */ 212 cvox.Api.speak = function(textString, queueMode, properties) { 213 if (!cvox.Api.isChromeVoxActive()) { 214 return; 215 } 216 217 if (implementation_) { 218 implementation_.speak(textString, queueMode, properties); 219 } else { 220 var message = { 221 'cmd': 'speak', 222 'args': [textString, queueMode, properties] 223 }; 224 callSpeakAsync_(message, properties); 225 } 226 }; 227 228 /** 229 * Speaks a description of the given node. 230 * 231 * @param {Node} targetNode A DOM node to speak. 232 * @param {number=} queueMode Valid modes are 0 for flush; 1 for queue. 233 * @param {Object=} properties Speech properties to use for this utterance. 234 */ 235 cvox.Api.speakNode = function(targetNode, queueMode, properties) { 236 if (!cvox.Api.isChromeVoxActive()) { 237 return; 238 } 239 240 if (implementation_) { 241 implementation_.speak(cvox.DomUtil.getName(targetNode), 242 queueMode, properties); 243 } else { 244 var message = { 245 'cmd': 'speakNodeRef', 246 'args': [cvox.ApiUtils.makeNodeReference(targetNode), queueMode, 247 properties] 248 }; 249 callSpeakAsync_(message, properties); 250 } 251 }; 252 253 /** 254 * Stops speech. 255 */ 256 cvox.Api.stop = function() { 257 if (!cvox.Api.isChromeVoxActive()) { 258 return; 259 } 260 261 if (implementation_) { 262 implementation_.stop(); 263 } else { 264 var message = { 265 'cmd': 'stop' 266 }; 267 channel_.port1.postMessage(JSON.stringify(message)); 268 } 269 }; 270 271 /** 272 * Plays the specified earcon sound. 273 * 274 * @param {string} earcon An earcon name. 275 * Valid names are: 276 * ALERT_MODAL 277 * ALERT_NONMODAL 278 * BULLET 279 * BUSY_PROGRESS_LOOP 280 * BUSY_WORKING_LOOP 281 * BUTTON 282 * CHECK_OFF 283 * CHECK_ON 284 * COLLAPSED 285 * EDITABLE_TEXT 286 * ELLIPSIS 287 * EXPANDED 288 * FONT_CHANGE 289 * INVALID_KEYPRESS 290 * LINK 291 * LISTBOX 292 * LIST_ITEM 293 * NEW_MAIL 294 * OBJECT_CLOSE 295 * OBJECT_DELETE 296 * OBJECT_DESELECT 297 * OBJECT_OPEN 298 * OBJECT_SELECT 299 * PARAGRAPH_BREAK 300 * SEARCH_HIT 301 * SEARCH_MISS 302 * SECTION 303 * TASK_SUCCESS 304 * WRAP 305 * WRAP_EDGE 306 * This list may expand over time. 307 */ 308 cvox.Api.playEarcon = function(earcon) { 309 if (!cvox.Api.isChromeVoxActive()) { 310 return; 311 } 312 if (implementation_) { 313 implementation_.playEarcon(earcon); 314 } else { 315 var message = { 316 'cmd': 'playEarcon', 317 'args': [earcon] 318 }; 319 channel_.port1.postMessage(JSON.stringify(message)); 320 } 321 }; 322 323 /** 324 * Synchronizes ChromeVox's internal cursor to the targetNode. 325 * Note that this will NOT trigger reading unless given the 326 * optional argument; it is for setting the internal ChromeVox 327 * cursor so that when the user resumes reading, they will be 328 * starting from a reasonable position. 329 * 330 * @param {Node} targetNode The node that ChromeVox should be synced to. 331 * @param {boolean=} speakNode If true, speaks out the node. 332 */ 333 cvox.Api.syncToNode = function(targetNode, speakNode) { 334 if (!cvox.Api.isChromeVoxActive() || !targetNode) { 335 return; 336 } 337 338 if (implementation_) { 339 implementation_.syncToNode(targetNode, speakNode); 340 } else { 341 var message = { 342 'cmd': 'syncToNodeRef', 343 'args': [cvox.ApiUtils.makeNodeReference(targetNode), speakNode] 344 }; 345 channel_.port1.postMessage(JSON.stringify(message)); 346 } 347 }; 348 349 /** 350 * Retrieves the current node and calls the given callback function with it. 351 * 352 * @param {Function} callback The function to be called. 353 */ 354 cvox.Api.getCurrentNode = function(callback) { 355 if (!cvox.Api.isChromeVoxActive() || !callback) { 356 return; 357 } 358 359 if (implementation_) { 360 callback(cvox.ChromeVox.navigationManager.getCurrentNode()); 361 } else { 362 callAsync_({'cmd': 'getCurrentNode'}, function(response) { 363 callback(cvox.ApiUtils.getNodeFromRef(response['currentNode'])); 364 }); 365 } 366 }; 367 368 /** 369 * Specifies how the targetNode should be spoken using an array of 370 * NodeDescriptions. 371 * 372 * @param {Node} targetNode The node that the NodeDescriptions should be 373 * spoken using the given NodeDescriptions. 374 * @param {Array.<Object>} nodeDescriptions The Array of 375 * NodeDescriptions for the given node. 376 */ 377 cvox.Api.setSpeechForNode = function(targetNode, nodeDescriptions) { 378 if (!cvox.Api.isChromeVoxActive() || !targetNode || !nodeDescriptions) { 379 return; 380 } 381 targetNode.setAttribute('cvoxnodedesc', JSON.stringify(nodeDescriptions)); 382 }; 383 384 /** 385 * Simulate a click on an element. 386 * 387 * @param {Element} targetElement The element that should be clicked. 388 * @param {boolean} shiftKey Specifies if shift is held down. 389 */ 390 cvox.Api.click = function(targetElement, shiftKey) { 391 if (!cvox.Api.isChromeVoxActive() || !targetElement) { 392 return; 393 } 394 395 if (implementation_) { 396 cvox.DomUtil.clickElem(targetElement, shiftKey, true); 397 } else { 398 var message = { 399 'cmd': 'clickNodeRef', 400 'args': [cvox.ApiUtils.makeNodeReference(targetElement), shiftKey] 401 }; 402 channel_.port1.postMessage(JSON.stringify(message)); 403 } 404 }; 405 406 /** 407 * Returns the build info. 408 * 409 * @param {function(string)} callback Function to receive the build info. 410 */ 411 cvox.Api.getBuild = function(callback) { 412 if (!cvox.Api.isChromeVoxActive() || !callback) { 413 return; 414 } 415 if (implementation_) { 416 callback(cvox.BuildInfo.build); 417 } else { 418 callAsync_({'cmd': 'getBuild'}, function(response) { 419 callback(response['build']); 420 }); 421 } 422 }; 423 424 /** 425 * Returns the ChromeVox version, a string of the form 'x.y.z', 426 * like '1.18.0'. 427 * 428 * @param {function(string)} callback Function to receive the version. 429 */ 430 cvox.Api.getVersion = function(callback) { 431 if (!cvox.Api.isChromeVoxActive() || !callback) { 432 return; 433 } 434 if (implementation_) { 435 callback(cvox.ChromeVox.version + ''); 436 } else { 437 callAsync_({'cmd': 'getVersion'}, function(response) { 438 callback(response['version']); 439 }); 440 } 441 }; 442 443 /** 444 * Returns the key codes of the ChromeVox modifier keys. 445 * @param {function(Array.<number>)} callback Function to receive the keys. 446 */ 447 cvox.Api.getCvoxModifierKeys = function(callback) { 448 if (!cvox.Api.isChromeVoxActive() || !callback) { 449 return; 450 } 451 if (implementation_) { 452 callback(cvox.KeyUtil.cvoxModKeyCodes()); 453 } else { 454 callAsync_({'cmd': 'getCvoxModKeys'}, function(response) { 455 callback(response['keyCodes']); 456 }); 457 } 458 }; 459 460 /** 461 * Returns if ChromeVox will handle this key event. 462 * @param {Event} keyEvent A key event. 463 * @param {function(boolean)} callback Function to receive the keys. 464 */ 465 cvox.Api.isKeyShortcut = function(keyEvent, callback) { 466 if (!callback) { 467 return; 468 } 469 if (!cvox.Api.isChromeVoxActive()) { 470 callback(false); 471 return; 472 } 473 /* TODO(peterxiao): Ignore these keys until we do this in a smarter way. */ 474 var KEY_IGNORE_LIST = [ 475 37, /* Left arrow. */ 476 39 /* Right arrow. */ 477 ]; 478 if (KEY_IGNORE_LIST.indexOf(keyEvent.keyCode) && !keyEvent.altKey && 479 !keyEvent.shiftKey && !keyEvent.ctrlKey && !keyEvent.metaKey) { 480 callback(false); 481 return; 482 } 483 484 if (implementation_) { 485 var keySeq = cvox.KeyUtil.keyEventToKeySequence(keyEvent); 486 callback(cvox.ChromeVoxKbHandler.handlerKeyMap.hasKey(keySeq)); 487 } else { 488 var strippedKeyEvent = {}; 489 /* Blacklist these props so we can safely stringify. */ 490 var BLACK_LIST_PROPS = ['target', 'srcElement', 'currentTarget', 'view']; 491 for (var prop in keyEvent) { 492 if (BLACK_LIST_PROPS.indexOf(prop) === -1) { 493 strippedKeyEvent[prop] = keyEvent[prop]; 494 } 495 } 496 var message = { 497 'cmd': 'isKeyShortcut', 498 'args': [strippedKeyEvent] 499 }; 500 callAsync_(message, function(response) { 501 callback(response['isHandled']); 502 }); 503 } 504 }; 505 506 /** 507 * Set key echoing on key press. 508 * @param {boolean} keyEcho Whether key echoing should be on or off. 509 */ 510 cvox.Api.setKeyEcho = function(keyEcho) { 511 if (!cvox.Api.isChromeVoxActive()) { 512 return; 513 } 514 515 if (implementation_) { 516 implementation_.setKeyEcho(keyEcho); 517 } else { 518 var message = { 519 'cmd': 'setKeyEcho', 520 'args': [keyEcho] 521 }; 522 channel_.port1.postMessage(JSON.stringify(message)); 523 } 524 }; 525 526 /** 527 * Exports the ChromeVox math API. 528 * TODO(dtseng, sorge): Requires more detailed documentation for class 529 * members. 530 * @constructor 531 */ 532 cvox.Api.Math = function() {}; 533 534 // TODO(dtseng, sorge): This need not be specific to math; once speech engine 535 // stabilizes, we can generalize. 536 // TODO(dtseng, sorge): This API is way too complicated; consolidate args 537 // when re-thinking underlying representation. Some of the args don't have a 538 // well-defined purpose especially for a caller. 539 /** 540 * Defines a math speech rule. 541 * @param {string} name Rule name. 542 * @param {string} dynamic Dynamic constraint annotation. In the case of a 543 * math rule it consists of a domain.style string. 544 * @param {string} action An action of rule components. 545 * @param {string} prec XPath or custom function constraining match. 546 * @param {...string} constraints Additional constraints. 547 */ 548 cvox.Api.Math.defineRule = 549 function(name, dynamic, action, prec, constraints) { 550 if (!cvox.Api.isChromeVoxActive()) { 551 return; 552 } 553 var constraintList = Array.prototype.slice.call(arguments, 4); 554 var args = [name, dynamic, action, prec].concat(constraintList); 555 if (implementation_) { 556 implementation_.Math.defineRule.apply(implementation_.Math, args); 557 } else { 558 var msg = {'cmd': 'Math.defineRule', args: args}; 559 channel_.port1.postMessage(JSON.stringify(msg)); 560 } 561 }; 562 563 cvox.Api.internalEnable(); 564 565 /** 566 * NodeDescription 567 * Data structure for holding information on how to speak a particular node. 568 * NodeDescriptions will be converted into NavDescriptions for ChromeVox. 569 * 570 * The string data is separated into context, text, userValue, and annotation 571 * to enable ChromeVox to speak each of these with the voice settings that 572 * are consistent with how ChromeVox normally presents information about 573 * nodes to users. 574 * 575 * @param {string} context Contextual information that the user should 576 * hear first which is not part of main content itself. For example, 577 * the user/date of a given post. 578 * @param {string} text The main content of the node. 579 * @param {string} userValue Anything that the user has entered. 580 * @param {string} annotation The role and state of the object. 581 */ 582 // TODO (clchen, deboer): Put NodeDescription into externs for developers 583 // building ChromeVox extensions. 584 cvox.NodeDescription = function(context, text, userValue, annotation) { 585 this.context = context ? context : ''; 586 this.text = text ? text : ''; 587 this.userValue = userValue ? userValue : ''; 588 this.annotation = annotation ? annotation : ''; 589 }; 590})(); 591