• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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