api.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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