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