Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(18)

Side by Side Diff: chrome/browser/resources/access_chromevox/scripts/axsjax/axsnav.js

Issue 6254007: Adding ChromeVox as a component extensions (enabled only for ChromeOS, for no... (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: '' Created 9 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
Property Changes:
Added: svn:executable
+ *
Added: svn:eol-style
+ LF
OLDNEW
(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 goog.provide('cvox.AxsNav');
6
7 goog.require('cvox.CustomWalker');
8
9 /**
10 * @fileoverview AxsNav - JavaScript library for enhancing the navigation
11 * of content on web pages.
12 */
13
14 /**
15 * Class for managing navigation of website content
16 *
17 * @param {Object}
18 * axsJAXObj An instance of an AxsJAX object.
19 * @constructor
20 */
21 cvox.AxsNav = function(axsJAXObj) {
22 this.nextListKeys = '';
23 this.prevListKeys = '';
24 this.navArray = new Array();
25 this.navListIdx = 0;
26 this.navItemIdxs = new Array();
27 this.lastItem = null;
28 this.targetsArray = new Array();
29 this.topCharCodeMap = new Object();
30 this.topKeyCodeMap = new Object();
31 this.charCodeMaps = new Array();
32 this.keyCodeMaps = new Array();
33 this.axs_ = axsJAXObj;
34 this.lens_ = null;
35 this.pk_ = null;
36 this.snd_ = null;
37 this.LIST_SND = 'list';
38 this.ITEM_SND = 'item';
39 this.WRAP_SND = 'wrap';
40 this.keyHandler = null;
41 this.managedCompletionListName = null;
42 this.titleToEarconMap = new Object();
43 this.titleToEarconMap[cvox.AxsNav.str.CATEGORY_EARCONS_LIST] = this.LIST_SND;
44 this.titleToEarconMap[cvox.AxsNav.str.CATEGORY_EARCONS_ITEM] = this.ITEM_SND;
45 this.titleToEarconMap[cvox.AxsNav.str.CATEGORY_EARCONS_WRAP] = this.WRAP_SND;
46 };
47
48 /**
49 * Object that contains all string literals used by cvox.AxsNav.
50 *
51 * @type {Object}
52 */
53 cvox.AxsNav.str = {
54 NEXT_LIST: 'Next list',
55 PREV_LIST: 'Previous list',
56 FORWARD_ITEM: 'Forward item',
57 BACKWARDS_ITEM: 'Backwards item',
58 CYCLE_NEXT_ITEM: 'Cycle next item',
59 CYCLE_PREV_ITEM: 'Cycle previous item',
60 SELECT_ACTION: 'Select available action',
61 NO_AVAILABLE_ACTION: 'No available actions',
62 PGUP: 'Page up',
63 PGDOWN: 'Page down',
64 ENTER: 'Enter',
65 ESC: 'Escape',
66 DEL: 'Delete',
67 UP: 'Up',
68 DOWN: 'Down',
69 LEFT: 'Left',
70 RIGHT: 'Right',
71 OR: 'or',
72 CATEGORY_NAVIGATION_ACTIONS: 'Navigation actions',
73 CATEGORY_APPLICATION_ACTIONS: 'Application actions',
74 CATEGORY_EARCONS: 'Earcons',
75 CATEGORY_EARCONS_LIST: 'List selected',
76 CATEGORY_EARCONS_ITEM: 'Item selected',
77 CATEGORY_EARCONS_WRAP: 'List wrap',
78 CATEGORY_HELP_INSTRUCTIONS: 'Help for Javascript enhanced Web content. ' +
79 'Use arrows to explore.',
80 CATEGORY_HELP_INSTRUCTIONS_EXPLORE_HELP: 'Use the left and right ' +
81 'arrows to explore the help categories and the down and up arrows ' +
82 'to explore the current category.',
83 CATEGORY_HELP_INSTRUCTIONS_STRUCTURE: 'The page content is divided ' +
84 'into lists with similar items. A user can switch the current list ' +
85 'and traverse it.',
86 CATEGORY_HELP_INSTRUCTIONS_SHORTCUTS: 'There are global shotcuts that ' +
87 'can be triggered from any list and local shortcuts that can be ' +
88 'triggered only from the current list.',
89 CATEGORY_HELP_INSTRUCTIONS_SELECTOR: 'Press dot anytime to show an ' +
90 'available actions selector.'
91 };
92
93 /**
94 * Set the PowerKey object used for displaying the valid actions in a given
95 * context. The PowerKey auto completion input element is invoked via a
96 * shortcutKey.
97 *
98 * @param {Object}
99 * powerKeyObj A PowerKey object.
100 * @param {string}
101 * shortcutKey A key for invoking PowerKey.
102 */
103 cvox.AxsNav.prototype.setPowerKey = function(powerKeyObj, shortcutKey) {
104 if (shortcutKey) {
105 this.pk_ = powerKeyObj;
106 // Initialize the PowerKey object if it has not been initialized yet
107 if (this.pk_.cmpTextElement === null) {
108 this.defaultInitPowerKeyObj();
109 }
110 var self = this;
111 var keyArray = new Array(shortcutKey);
112 this.assignKeysToMethod(keyArray, this.topCharCodeMap,
113 this.topKeyCodeMap, function() {
114 self.showAvailableActionsSelector();
115 });
116 }
117 };
118
119 /**
120 * Returns whether the specified navigation list is valid. If the navigation
121 * list is dynamic and appears to not be valid, this function will try to reload
122 * it and check whether or not it becomes valid.
123 *
124 * @param {Object}
125 * navList The specified list object to check.
126 * @return {boolean} Whether the specified list object is valid.
127 */
128 cvox.AxsNav.prototype.validateList = function(navList) {
129 var valid = true;
130 // Reload dynamic lists
131 if ((navList.type == 'dynamic') && (navList.items.length === 0)) {
132 // Clear the lens to avoid its contents interfering with the xpath
133 if (this.lens_ !== null) {
134 this.lens_.view(null);
135 }
136 navList.items = this.makeItemsArray(navList.origListObj);
137 navList.targets = this.makeTargetsArray(navList.origListObj);
138 // Reset the nav index of the list being validated.
139 // Most of the time, the list being validated is the same
140 // as the current list.
141 if (this.navArray[this.navListIdx] === navList) {
142 this.navItemIdxs[this.navListIdx] = -1;
143 } else {
144 for (var i = 0, tempList; tempList = this.navArray[i]; i++) {
145 if (tempList === navList) {
146 this.navItemIdxs[i] = -1;
147 break;
148 }
149 }
150 }
151 }
152 if ((navList.items.length === 0) && (navList.entryTarget === null)) {
153 valid = false;
154 }
155 return valid;
156 };
157
158 /**
159 * Performs the specified action with a list is switched into.
160 */
161 cvox.AxsNav.prototype.doListEntryActions = function() {
162 var currentList = this.currentList();
163 var target = currentList.entryTarget;
164 var func = null;
165 if (target !== null) {
166 this.actOnTarget(target);
167 func = this.getCallbackFunction(target.action);
168 }
169 if (func === null) {
170 this.announceCurrentList();
171 if (this.snd_ !== null) {
172 this.snd_.play(this.LIST_SND);
173 }
174 }
175 };
176
177 /**
178 * Goes to the next navigation list and returns it
179 *
180 * @return {Object?} The next navigation list.
181 */
182 cvox.AxsNav.prototype.nextList = function() {
183 if (this.navArray.length < 1) {
184 return null;
185 }
186 // Find the next list with items
187 for (var i = 0, list; list = this.navArray[i]; i++) {
188 this.navListIdx++;
189 if (this.navListIdx >= this.navArray.length) {
190 this.navListIdx = 0;
191 }
192 if (this.validateList(this.navArray[this.navListIdx])) {
193 break;
194 }
195 }
196 return this.currentList();
197 };
198
199 /**
200 * Goes to the previous navigation list and returns it
201 *
202 * @return {Object?} The previous navigation list.
203 */
204 cvox.AxsNav.prototype.prevList = function() {
205 if (this.navArray.length < 1) {
206 return null;
207 }
208 // Find the next list with item
209 for (var i = 0, list; list = this.navArray[i]; i++) {
210 this.navListIdx--;
211 if (this.navListIdx < 0) {
212 this.navListIdx = this.navArray.length - 1;
213 }
214 if (this.validateList(this.navArray[this.navListIdx])) {
215 break;
216 }
217 }
218 return this.currentList();
219 };
220
221 /**
222 * Returns the current navigation list.
223 *
224 * @return {Object} The current navigation list.
225 */
226 cvox.AxsNav.prototype.currentList = function() {
227 return this.navArray[this.navListIdx];
228 };
229
230 /**
231 * Speaks the title of the current list
232 */
233 cvox.AxsNav.prototype.announceCurrentList = function() {
234 this.axs_.speakTextViaNode(this.currentList().title);
235 };
236
237 /**
238 * Goes to the next item and returns it; if there is no next item, this will
239 * wrap to the first item in the list.
240 *
241 * @return {Object?} The next item. Use item.elem to get at the DOM node.
242 */
243 cvox.AxsNav.prototype.nextItem = function() {
244 if (this.navArray.length < 1) {
245 return null;
246 }
247 var currentList = this.navArray[this.navListIdx];
248 var items = currentList.items;
249 if (items.length < 1) {
250 return null;
251 }
252 if (this.lastItem) {
253 var currentListId = this.getListId(currentList);
254 var syncedIndex = this.lastItem.elem.AxsNavInfo[currentListId];
255 if (typeof (syncedIndex) != 'undefined') {
256 this.navItemIdxs[this.navListIdx] = syncedIndex;
257 }
258 }
259 this.navItemIdxs[this.navListIdx]++;
260 var looped = false;
261 if (this.navItemIdxs[this.navListIdx] >= items.length) {
262 this.navItemIdxs[this.navListIdx] = 0;
263 looped = true;
264 }
265 var itemIndex = this.navItemIdxs[this.navListIdx];
266 // Perform a validity check to determine if the xpaths should be
267 // re-evaluated
268 if (items[itemIndex].elem.parentNode === null) {
269 // Clear the lens to avoid its contents interfering with the xpath
270 if (this.lens_ !== null) {
271 this.lens_.view(null);
272 }
273 currentList.items = this.makeItemsArray(currentList.origListObj);
274 this.navItemIdxs[this.navListIdx] = 0;
275 itemIndex = this.navItemIdxs[this.navListIdx];
276 }
277 this.lastItem = items[itemIndex];
278 if (this.snd_ !== null) {
279 if (looped) {
280 this.snd_.play(this.WRAP_SND);
281 } else {
282 this.snd_.play(this.ITEM_SND);
283 }
284 }
285 return this.lastItem;
286 };
287
288 /**
289 * Goes to the next item and returns it; if this causes wrapping and there is a
290 * tailTarget on the list, then this will act on that target and return null
291 * instead.
292 *
293 * @return {Object?} The next item. Use item.elem to get at the DOM node.
294 */
295 cvox.AxsNav.prototype.fwdItem = function() {
296 var list = this.navArray[this.navListIdx];
297 var index = this.navItemIdxs[this.navListIdx];
298 if ((list.tailTarget !== null) && (index + 1 >= list.items.length)) {
299 this.actOnTarget(list.tailTarget);
300 this.navItemIdxs[this.navListIdx] = 0;
301 return null;
302 }
303 var item = this.nextItem();
304 return item;
305 };
306
307 /**
308 * Goes to the previous item and returns it; if there is no previous item, this
309 * will wrap to the last item in the list.
310 *
311 * @return {Object?} The previous item. Use item.elem to get at the DOM node.
312 */
313 cvox.AxsNav.prototype.prevItem = function() {
314 if (this.navArray.length < 1) {
315 return null;
316 }
317 var currentList = this.navArray[this.navListIdx];
318 var items = currentList.items;
319 if (items.length < 1) {
320 return null;
321 }
322 if (this.lastItem) {
323 var currentListId = this.getListId(currentList);
324 var syncedIndex = this.lastItem.elem.AxsNavInfo[currentListId];
325 if (typeof (syncedIndex) != 'undefined') {
326 this.navItemIdxs[this.navListIdx] = syncedIndex;
327 }
328 }
329 this.navItemIdxs[this.navListIdx]--;
330 var looped = false;
331 if (this.navItemIdxs[this.navListIdx] < 0) {
332 this.navItemIdxs[this.navListIdx] = items.length - 1;
333 looped = true;
334 }
335 var itemIndex = this.navItemIdxs[this.navListIdx];
336 // Perform a validity check to determine if the xpaths should be
337 // re-evaluated
338 if (items[itemIndex].elem.parentNode === null) {
339 // Clear the lens to avoid its contents interfering with the xpath
340 if (this.lens_ !== null) {
341 this.lens_.view(null);
342 }
343 currentList.items = this.makeItemsArray(currentList.origListObj);
344 this.navItemIdxs[this.navListIdx] = currentList.items.length;
345 itemIndex = this.navItemIdxs[this.navListIdx];
346 }
347 this.lastItem = items[itemIndex];
348 if (this.snd_ !== null) {
349 if (looped) {
350 this.snd_.play(this.WRAP_SND);
351 } else {
352 this.snd_.play(this.ITEM_SND);
353 }
354 }
355 return this.lastItem;
356 };
357
358 /**
359 * Goes to the previous item and returns it; if this causes wrapping and there
360 * is a headTarget on the list, then this will act on that target and return
361 * null instead.
362 *
363 * @return {Object?} The previous item. Use item.elem to get at the DOM node.
364 */
365 cvox.AxsNav.prototype.backItem = function() {
366 var list = this.navArray[this.navListIdx];
367 var index = this.navItemIdxs[this.navListIdx];
368 if ((list.headTarget !== null) && (index <= 0)) {
369 this.actOnTarget(list.headTarget);
370 this.navItemIdxs[this.navListIdx] = list.items.length - 1;
371 return null;
372 }
373 var item = this.prevItem();
374 return item;
375 };
376
377 /**
378 * Returns the current item.
379 *
380 * @return {Object?} The current item. Use item.elem to get at the DOM node.
381 */
382 cvox.AxsNav.prototype.currentItem = function() {
383 if (this.navArray.length < 1) {
384 return null;
385 }
386 var currentList = this.navArray[this.navListIdx];
387 if (this.lastItem) {
388 var currentListId = this.getListId(currentList);
389 var syncedIndex = this.lastItem.elem.AxsNavInfo[currentListId];
390 if (typeof (syncedIndex) != 'undefined') {
391 this.navItemIdxs[this.navListIdx] = syncedIndex;
392 }
393 }
394 var items = currentList.items;
395 var itemIndex = this.navItemIdxs[this.navListIdx];
396 this.lastItem = items[itemIndex];
397 return this.lastItem;
398 };
399
400 /**
401 * Returns the callback function if the action is a valid callback; returns null
402 * otherwise.
403 *
404 * @param {String?}
405 * actionString The action string for an item or target.
406 * @return {Function?} The callback function if there is a valid one.
407 */
408 cvox.AxsNav.prototype.getCallbackFunction = function(actionString) {
409 var callbackFunc = null;
410 if ((actionString !== null) && actionString.indexOf &&
411 (actionString.indexOf('CALL:') === 0) &&
412 (actionString.indexOf('(') === -1)) {
413 try {
414 callbackFunc = /** @type {Function} */
415 (eval(actionString.substring(5)));
416 } catch (e) {
417 }
418 }
419 return callbackFunc;
420 };
421
422 /**
423 * This function will act on the item based on what action was specified in the
424 * Content Navigation Listing.
425 *
426 * @param {Object?}
427 * item The item to act on. Use item.elem to get at the DOM node.
428 */
429 cvox.AxsNav.prototype.actOnItem = function(item) {
430 if (item !== null) {
431 var self = this;
432 var doAction = function() {
433 var func = self.getCallbackFunction(item.action);
434 if (func) {
435 func(item);
436 } else {
437 if (self.lens_ !== null) {
438 self.lens_.view(item.elem);
439 }
440 self.axs_.goTo(item.elem);
441 }
442 };
443 // If there is a node that was focused, unfocus it so that
444 // any keys the user presses after using the nav system will not
445 // be sent to the wrong place.
446 if (this.axs_.lastFocusedNode && this.axs_.lastFocusedNode.blur) {
447 var oldNode = this.axs_.lastFocusedNode;
448 // Set the lastFocusedNode to null to prevent AxsJAX's blur handler
449 // from kicking in as that blur handler will conflict with the
450 // temporary blur handler which results in screen readers not
451 // speaking properly due to how the eventing system works.
452 // Because we are not allowing the regular blur handler to work,
453 // we need to make sure that we do the same work of cleaning up.
454 this.axs_.lastFocusedNode = null;
455 if (oldNode.removeAttribute) {
456 this.axs_.removeAttributeOf(oldNode, 'aria-activedescendant');
457 }
458 // The action needs to be done inside a temporary blur handler
459 // because otherwise, there is a timing issue of when the events
460 // get sent and screen readers won't speak.
461 var tempBlurHandler = function(evt) {
462 evt.target.removeEventListener('blur', tempBlurHandler, true);
463 doAction();
464 };
465 oldNode.addEventListener('blur', tempBlurHandler, true);
466 oldNode.blur();
467 } else {
468 doAction();
469 }
470 } else {
471 var currentList = this.navArray[this.navListIdx];
472 if (currentList.type == 'dynamic') {
473 this.axs_.speakTextViaNode(currentList.onEmpty);
474 }
475 }
476 };
477
478 /**
479 * This function creates the maps keypresses to a method on a given char and key
480 * map.
481 *
482 * @param {Array}
483 * keyArray Array of keys that will be associated with the method.
484 * @param {Object}
485 * charMap Dictionary that maps character codes to methods.
486 * @param {Object}
487 * keyMap Dictionary that maps key codes to methods.
488 * @param {Function}
489 * method Method to be be associated with the array of keys.
490 */
491 cvox.AxsNav.prototype.assignKeysToMethod = function(keyArray, charMap, keyMap,
492 method) {
493 for (var i = 0, key; key = keyArray[i]; i++) {
494 if (key == 'LEFT') {
495 keyMap[37] = method;
496 } else if (key == 'UP') {
497 keyMap[38] = method;
498 } else if (key == 'RIGHT') {
499 keyMap[39] = method;
500 } else if (key == 'DOWN') {
501 keyMap[40] = method;
502 } else if (key == 'PGUP') {
503 keyMap[33] = method;
504 } else if (key == 'PGDOWN') {
505 keyMap[34] = method;
506 } else if (key == 'ENTER') {
507 keyMap[13] = method;
508 } else if (key == 'ESC') {
509 keyMap[27] = method;
510 } else if (key == 'DEL') {
511 keyMap[46] = method;
512 } else {
513 charMap[key.charCodeAt(0)] = method;
514 }
515 }
516 };
517
518 /**
519 * This function creates the mapping between keypresses and navigation behavior
520 * for item keys. This mapping is only active when the user is in the
521 * corresponding navList.
522 *
523 * @param {string}
524 * keyStr String that indicates the keys to be used.
525 * @param {number}
526 * navListIdx Index of the list that these keypresses are associated
527 * with.
528 * @param {string}
529 * navTaskStr "next","prev","fwd","back".
530 */
531 cvox.AxsNav.prototype.assignItemKeys = function(keyStr, navListIdx,
532 navTaskStr) {
533 var keys = new Array();
534 if (keyStr === '') {
535 return;
536 }
537 keys = keyStr.split(' ');
538 var self = this;
539 if (navTaskStr == 'prev') {
540 this.assignKeysToMethod(keys, this.charCodeMaps[navListIdx],
541 this.keyCodeMaps[navListIdx], function() {
542 self.actOnItem(self.prevItem());
543 });
544 } else if (navTaskStr == 'next') {
545 this.assignKeysToMethod(keys, this.charCodeMaps[navListIdx],
546 this.keyCodeMaps[navListIdx], function() {
547 self.actOnItem(self.nextItem());
548 });
549 } else if (navTaskStr == 'back') {
550 this.assignKeysToMethod(keys, this.charCodeMaps[navListIdx],
551 this.keyCodeMaps[navListIdx], function() {
552 var backItem = self.backItem();
553 if (backItem !== null) {
554 self.actOnItem(backItem);
555 }
556 });
557 } else {
558 this.assignKeysToMethod(keys, this.charCodeMaps[navListIdx],
559 this.keyCodeMaps[navListIdx], function() {
560 var fwdItem = self.fwdItem();
561 if (fwdItem != null) {
562 self.actOnItem(fwdItem);
563 }
564 });
565 }
566 };
567
568 /**
569 * This function creates the mapping between keypresses and navigation behavior
570 * for hotkeys. This mapping is active all the time, regardless of which navList
571 * the user is in.
572 *
573 * @param {string}
574 * keyStr String that indicates the keys to be used. Pressing these
575 * keys will cause the user to jump to the list that the key is
576 * associated with and read the next item there.
577 * @param {number}
578 * navListIdx Index of the list that these keypresses are associated
579 * with.
580 * @param {string}
581 * emptyMsg String to speak to the user if the list is empty.
582 */
583 cvox.AxsNav.prototype.assignHotKeys = function(keyStr, navListIdx, emptyMsg) {
584 var keys = new Array();
585 if (keyStr === '') {
586 return;
587 }
588 keys = keyStr.split(' ');
589 var self = this;
590 this.assignKeysToMethod(keys, this.topCharCodeMap, this.topKeyCodeMap,
591 function() {
592 if (!self.validateList(self.navArray[navListIdx])) {
593 self.axs_.speakTextViaNode(emptyMsg);
594 return;
595 }
596 self.navListIdx = navListIdx;
597 self.actOnItem(self.nextItem());
598 });
599 };
600
601 /**
602 * For all keys that map to lists with no items, those keys should speak some
603 * message to let the user know that the function was called but was
604 * unsuccessful because there is no content.
605 *
606 * @param {string}
607 * keyStr String that indicates the keys to be used.
608 *
609 * @param {string}
610 * emptyMsg The message that should be spoken when the user presses
611 * the key(s) to let them know that there is no content.
612 */
613 cvox.AxsNav.prototype.assignEmptyMsgKeys = function(keyStr, emptyMsg) {
614 var keys = new Array();
615 if (keyStr === '') {
616 return;
617 }
618 keys = keyStr.split(' ');
619 var self = this;
620 this.assignKeysToMethod(keys, this.topCharCodeMap, this.topKeyCodeMap,
621 function() {
622 self.axs_.speakTextViaNode(emptyMsg);
623 });
624
625 };
626
627 /**
628 * This function creates the mapping between keypresses and target nodes. This
629 * mapping is active all the time, regardless of which navList the user is in.
630 *
631 * @param {Object}
632 * target Target object created from the <target> element.
633 * @param {Object}
634 * charMap Dictionary that maps character codes to methods.
635 * @param {Object}
636 * keyMap Dictionary that maps key codes to methods.
637 */
638 cvox.AxsNav.prototype.assignTargetKeys = function(target, charMap, keyMap) {
639 var keys = new Array();
640 if (target.hotkey === '') {
641 return;
642 }
643 keys = target.hotkey.split(' ');
644 var self = this;
645 this.assignKeysToMethod(keys, charMap, keyMap, function() {
646 self.actOnTarget(target);
647 });
648 };
649
650 /**
651 * This function will act on the target specified.
652 *
653 * @param {?Object}
654 * target The target to act on.
655 */
656 cvox.AxsNav.prototype.actOnTarget = function(target) {
657 if (target.action == 'click') {
658 this.clickOnTarget(target);
659 } else {
660 this.callActionOnTarget(target);
661 }
662 };
663
664 /**
665 * Calls the appropriate action on a target.
666 *
667 * @param {Object}
668 * target the target to act upon.
669 */
670 cvox.AxsNav.prototype.callActionOnTarget = function(target) {
671 var elems = this.getElementsForTarget(target);
672 if (elems.length < 1) {
673 this.axs_.speakTextViaNode(target.onEmpty);
674 } else {
675 var func = this.getCallbackFunction(target.action);
676 if (func) {
677 var item = new Object();
678 item.action = target.action;
679 item.elem = elems[0];
680 func(item);
681 this.axs_.markPosition(elems[0]);
682 } else {
683 throw new Error('Invalid action: ' + target.action);
684 }
685 }
686 };
687
688 /**
689 * Performs the click action of a target.
690 *
691 * @param {Object}
692 * target the target to act upon.
693 */
694 cvox.AxsNav.prototype.clickOnTarget = function(target) {
695 var elems = this.getElementsForTarget(target);
696 if (elems.length < 1) {
697 this.axs_.speakTextViaNode(target.onEmpty);
698 } else {
699 this.axs_.clickElem(elems[0], false);
700 elems[0].scrollIntoView(true);
701 this.axs_.markPosition(elems[0]);
702 }
703 };
704
705 /**
706 * Returns the elements referenced by a target
707 *
708 * @param {?Object}
709 * target a target element.
710 *
711 * @return {Array} an array of elements.
712 */
713 cvox.AxsNav.prototype.getElementsForTarget = function(target) {
714 var xpath = target.xpath;
715 var rootNode = this.axs_.getActiveDocument().documentElement;
716 if (xpath.indexOf('.') === 0) {
717 rootNode = this.currentItem().elem;
718 }
719 return this.axs_.evalXPath(xpath, rootNode);
720 };
721
722 /**
723 * Returns a function mapped to a key.
724 *
725 * @param {number}
726 * keyCode A key code.
727 * @param {number}
728 * charCode A char code.
729 * @return {Function?} A function mapped to the keyCode or charCode, undefined
730 * if the mapping does not exist.
731 */
732 cvox.AxsNav.prototype.getFunctionForKey = function(keyCode, charCode) {
733 var command = null;
734 var idx = this.navListIdx;
735 if (idx < this.keyCodeMaps.length) {
736 command = this.keyCodeMaps[idx][keyCode] ||
737 this.charCodeMaps[idx][charCode] || null;
738 }
739 if (command === null) {
740 command = this.topKeyCodeMap[keyCode] || this.topCharCodeMap[charCode];
741 }
742 return command;
743 };
744
745 /**
746 * Builds up the navigation system of lists of items. This system uses the idea
747 * of multiple cursors and the visitor pattern.
748 *
749 * @param {string}
750 * cnrString An XML string that contains the information needed to
751 * build up the content navigation rule.
752 *
753 * @notypecheck {Function?} opt_customNavMethod.
754 *
755 * @param {Function?}
756 * opt_customNavMethod A custom navigation method provided by the
757 * caller. This navigation method will be given the DOM created from
758 * the cnrString, the navigation array of lists of items, an array of
759 * all the lists which had zero items, and an an array of targets. If
760 * this is null, the default AxsJAX nav handler will be used.
761 */
762 cvox.AxsNav.prototype.navInit = function(cnrString, opt_customNavMethod) {
763 var cnrJson = new Object();
764 cnrJson.lists = new Array();
765
766 var parser = new DOMParser();
767 var cnrDOM = parser.parseFromString(cnrString, 'text/xml');
768
769 // Build up the navigation lists
770 var lists = cnrDOM.getElementsByTagName('list');
771
772 var i;
773 var listNode;
774 for (i = 0, listNode; listNode = lists[i]; i++) {
775 var navList = new Object();
776 navList.title = listNode.getAttribute('title');
777 navList.hotkey = listNode.getAttribute('hotkey');
778 navList.next = listNode.getAttribute('next');
779 navList.prev = listNode.getAttribute('prev');
780 navList.fwd = listNode.getAttribute('fwd');
781 navList.back = listNode.getAttribute('back');
782 navList.onEmpty = listNode.getAttribute('onEmpty');
783 navList.type = listNode.getAttribute('type');
784
785 var j;
786 var entry;
787 var k;
788 var attributes;
789 var length;
790 // Parse items to JSON
791 navList.items = new Array();
792 var cnrItems = listNode.getElementsByTagName('item');
793 for (j = 0; entry = cnrItems[j]; j++) {
794 var item = new Object();
795 item.xpath = entry.textContent;
796 if (entry.attributes instanceof NamedNodeMap) {
797 attributes = entry.attributes;
798 length = attributes.length;
799 for (k = 0; k < length; k++) {
800 var attrib = attributes.item(k);
801 item[attrib.nodeName] = attrib.value;
802 }
803 }
804 navList.items.push(item);
805 }
806 // Parse targets to JSON
807 navList.targets = new Array();
808 var cnrTargets = listNode.getElementsByTagName('target');
809 for (j = 0; entry = cnrTargets[j]; j++) {
810 var target = new Object();
811 target.xpath = entry.textContent;
812 if (entry.attributes instanceof NamedNodeMap) {
813 attributes = entry.attributes;
814 length = attributes.length;
815 for (k = 0; k < length; k++) {
816 var attrib = attributes.item(k);
817 target[attrib.nodeName] = attrib.value;
818 }
819 }
820 navList.targets.push(target);
821 }
822 cnrJson.lists.push(navList);
823 }
824
825 // Build up the targets
826 cnrJson.targets = new Array();
827 var currentNode;
828 var cnrNode = cnrDOM.firstChild;
829 for (i = 0, currentNode; currentNode = cnrNode.childNodes[i]; i++) {
830 if (currentNode.tagName == 'target') {
831 var target = new Object();
832 target.xpath = currentNode.textContent;
833 if (currentNode.attributes instanceof NamedNodeMap) {
834 attributes = currentNode.attributes;
835 length = attributes.length;
836 for (k = 0; k < length; k++) {
837 var attrib = attributes.item(k);
838 target[attrib.nodeName] = attrib.value;
839 }
840 }
841 cnrJson.targets.push(target);
842 }
843 }
844
845 // Get the next/prev list keys
846 cnrJson.next = cnrNode.getAttribute('next');
847 cnrJson.prev = cnrNode.getAttribute('prev');
848
849 if ((opt_customNavMethod === null) ||
850 (typeof (opt_customNavMethod) == 'undefined')) {
851 this.navInitJson(cnrJson, opt_customNavMethod);
852 } else {
853 // Wrapper function that will invoke the user's opt_customNavMethod
854 // This will be called when navInitJson is done processing
855 var func = new function(dummyJson, navArray, emptyLists, targetsArray) {
856 opt_customNavMethod(cnrNode, navArray, emptyLists, targetsArray);
857 };
858 this.navInitJson(cnrJson, func);
859 }
860 };
861
862 /**
863 * Generates a help string for the globally available keys. Keys which are
864 * specific to the current list are NOT included.
865 *
866 * @return {string} The help string for globally available keys.
867 */
868 cvox.AxsNav.prototype.globalHelpString = function() {
869 var globalActions = this.getGlobalActions();
870
871 var helpStr = '';
872 for (var i = 0, action; action = globalActions[i]; i++) {
873 helpStr = helpStr + action.keys + ', ' + action.title + '. ';
874 }
875
876 helpStr = helpStr + this.nextListKeys + ', ' + cvox.AxsNav.str.NEXT_LIST;
877 helpStr = helpStr + this.prevListKeys + ', ' + cvox.AxsNav.str.PREV_LIST;
878
879 // Make the keys sound nicer when spoken
880 helpStr = helpStr.replace('PGUP', cvox.AxsNav.str.PGUP);
881 helpStr = helpStr.replace('PGDOWN', cvox.AxsNav.str.PGDOWN);
882 helpStr = helpStr.replace('ENTER', cvox.AxsNav.str.ENTER);
883 helpStr = helpStr.replace('DEL', cvox.AxsNav.str.DELETE);
884 helpStr = helpStr.replace('UP', cvox.AxsNav.str.UP);
885 helpStr = helpStr.replace('DOWN', cvox.AxsNav.str.DOWN);
886 helpStr = helpStr.replace('LEFT', cvox.AxsNav.str.LEFT);
887 helpStr = helpStr.replace('RIGHT', cvox.AxsNav.str.RIGHT);
888
889 return helpStr;
890 };
891
892 /**
893 * Generates a help string for locally available keys.
894 *
895 * @return {string} The help string for locally available keys.
896 */
897 cvox.AxsNav.prototype.localHelpString = function() {
898 var localActions = this.getLocalActions();
899
900 var helpStr = '';
901 for (var i = 0, action; action = localActions[i]; i++) {
902 helpStr = helpStr + action.keys + ', ' + action.title + '. ';
903 }
904
905 var list = this.currentList();
906 if (list.nextKeys !== '') {
907 helpStr = helpStr + list.nextKeys + ', ' + cvox.AxsNav.str.CYCLE_NEXT_ITEM +
908 '. ';
909 }
910 if (list.prevKeys !== '') {
911 helpStr = helpStr + list.prevKeys + ', ' + cvox.AxsNav.str.CYCLE_PREV_ITEM +
912 '. ';
913 }
914 if (list.fwdKeys !== '') {
915 helpStr = helpStr + list.fwdKeys + ', ' + cvox.AxsNav.str.FORWARD_ITEM +
916 '. ';
917 }
918 if (list.backKeys !== '') {
919 helpStr = helpStr + list.backKeys + ', ' + cvox.AxsNav.str.BACKWARDS_ITEM +
920 '. ';
921 }
922
923 // Make the keys sound nicer when spoken
924 helpStr = helpStr.replace('PGUP', cvox.AxsNav.str.PGUP);
925 helpStr = helpStr.replace('PGDOWN', cvox.AxsNav.str.PGDOWN);
926 helpStr = helpStr.replace('ENTER', cvox.AxsNav.str.ENTER);
927 helpStr = helpStr.replace('DEL', cvox.AxsNav.str.DELETE);
928 helpStr = helpStr.replace('UP', cvox.AxsNav.str.UP);
929 helpStr = helpStr.replace('DOWN', cvox.AxsNav.str.DOWN);
930 helpStr = helpStr.replace('LEFT', cvox.AxsNav.str.LEFT);
931 helpStr = helpStr.replace('RIGHT', cvox.AxsNav.str.RIGHT);
932
933 return helpStr;
934 };
935
936 /**
937 * This function sets the lens to be used when going to an item's element.
938 *
939 * @param {Object?}
940 * lens The AxsLens object to be used. If null, no lens will be used.
941 */
942 cvox.AxsNav.prototype.setLens = function(lens) {
943 this.lens_ = lens;
944 };
945
946 /**
947 * This function sets the sound object to be used when going to an item's
948 * element.
949 *
950 * @param {Object?}
951 * snd The AxsSound object to be used. The AxsSound object should
952 * already have its verbosity set and be initialized. If null, no
953 * sound object will be used.
954 */
955 cvox.AxsNav.prototype.setSound = function(snd) {
956 this.snd_ = snd;
957 };
958
959 /**
960 * Refreshes the dynamic list with the specified title.
961 *
962 * @param {string?}
963 * listTitle The title of the list that should be refreshed.
964 * @return {boolean} True if the list was successfully refreshed.
965 */
966 cvox.AxsNav.prototype.refreshList = function(listTitle) {
967 if (listTitle === null) {
968 return false;
969 }
970 var reloaded = false;
971 var listId = this.findListIndexByTitle(listTitle);
972 if (listId > -1) {
973 var navList = this.navArray[listId];
974 navList.items = new Array();
975 navList.targets = new Array();
976 reloaded = this.validateList(navList);
977 }
978 return reloaded;
979 };
980
981 /**
982 * Disables the default keyboard handler for the AxsNav object by detaching it
983 * from the keypress event listener for the current document.
984 */
985 cvox.AxsNav.prototype.disableNavKeys = function() {
986 if (this.keyHandler !== null) {
987 document.removeEventListener('keypress', this.keyHandler, true);
988 }
989 };
990
991 /**
992 * Re-enables the default keyboard handler for the AxsNav object by reattaching
993 * it to the keypress event listener for the current document. This function
994 * assumes cvox.AxsNav.prototype.setUpNavKeys has already been called so that
995 * this.keyHandler is already setup and ready to go.
996 */
997 cvox.AxsNav.prototype.enableNavKeys = function() {
998 if (this.keyHandler !== null) {
999 // Remove it once so that the keyHandler is not accidentally added twice
1000 // just in case enableNavKeys has already been called.
1001 // If it has not already been added, this first removeEventListener call
1002 // is a no-op.
1003 document.removeEventListener('keypress', this.keyHandler, true);
1004 document.addEventListener('keypress', this.keyHandler, true);
1005 }
1006 };
1007
1008 /**
1009 * Shows a PowerKey input box for selecting an available action. Available
1010 * actions are those that have nodes when their xPaths are evaluated when this
1011 * function is called.
1012 */
1013 cvox.AxsNav.prototype.showAvailableActionsSelector = function() {
1014 // Fail silently if the PowerKey object is not set
1015 if (this.pk_ === null) {
1016 return;
1017 }
1018
1019 var actions = new Array();
1020 this.addLocalTargetActions_(actions);
1021 this.addGlobalTargetActions_(actions);
1022 if (actions.length === 0) {
1023 this.axs_.speakTextViaNode(cvox.AxsNav.str.NO_AVAILABLE_ACTION);
1024 return;
1025 }
1026
1027 var completionActionMap = new Object();
1028 for (var i = 0, action; action = actions[i]; i++) {
1029 action[this.pk_.context] = this.getFunctionForHotkeyStr_(action.keys);
1030 completionActionMap[action.title.toLowerCase()] = action;
1031 }
1032 this.pk_.setCompletionActionMap(completionActionMap);
1033
1034 var completionList = new Array();
1035 for (var i = 0, action; action = actions[i]; i++) {
1036 completionList.push(action.title);
1037 }
1038 this.pk_.setCompletionList(completionList);
1039
1040 this.managedCompletionListName = '';
1041 this.pk_.setBrowseOnly(false);
1042 this.pk_.setBrowseCallback(null);
1043 this.pk_.updateCompletionField(PowerKey.status.VISIBLE, true, 40, 20);
1044 };
1045
1046 /**
1047 * Shows a PowerKey read-only box for exploring the available actions.
1048 * The shown PowerKey instance that is composed of three categories:
1049 * <ol>
1050 * <li>
1051 * Global actions - actions that can be triggered from any list.
1052 * </li>
1053 * <li>
1054 * Local actions - actions that can be triggered only from the
1055 * current list.
1056 * </li>
1057 * <li>
1058 * Earcons - auditory clues used through the application.
1059 * </li>
1060 * </ol>
1061 * <strong>Note:</strong> If no PowerKey instance is set via a call
1062 * to cvox.AxsNav.prototype.stPowerKey(powerKey) calls to this method
1063 * are NOOP. If no AxsSound instance is set via a call to
1064 * cvox.AxsNav.prototype.setSound(axsSound) the Earcons category is
1065 * omitted.
1066 */
1067 cvox.AxsNav.prototype.showAvailableActionsHelp = function() {
1068 if (!this.pk_) {
1069 return;
1070 }
1071
1072 this.pk_.setBrowseOnly(true);
1073
1074 // help instructions
1075 var helpInstructionsTitles = new Array();
1076 helpInstructionsTitles.push(
1077 cvox.AxsNav.str.CATEGORY_HELP_INSTRUCTIONS_EXPLORE_HELP);
1078 helpInstructionsTitles.push(
1079 cvox.AxsNav.str.CATEGORY_HELP_INSTRUCTIONS_STRUCTURE);
1080 helpInstructionsTitles.push(
1081 cvox.AxsNav.str.CATEGORY_HELP_INSTRUCTIONS_SHORTCUTS);
1082 helpInstructionsTitles.push(
1083 cvox.AxsNav.str.CATEGORY_HELP_INSTRUCTIONS_SELECTOR);
1084 this.pk_.addCompletionListByName(cvox.AxsNav.str.CATEGORY_HELP_INSTRUCTIONS,
1085 helpInstructionsTitles, cvox.AxsNav.str.CATEGORY_HELP_INSTRUCTIONS);
1086
1087 // navigation actions category
1088 var navigationActions = new Array();
1089 this.addGlobalNavigationActions_(navigationActions);
1090 this.addLocalNavigationActions_(navigationActions);
1091 var navigationActionTitles = new Array();
1092 for (var i = 0, action; action = navigationActions[i]; i++) {
1093 navigationActionTitles.push(action.title);
1094 }
1095 this.pk_.addCompletionListByName(cvox.AxsNav.str.CATEGORY_NAVIGATION_ACTIONS,
1096 navigationActionTitles, cvox.AxsNav.str.CATEGORY_NAVIGATION_ACTIONS);
1097
1098 // application actions category
1099 var applicationActions = new Array();
1100 this.addGlobalTargetActions_(applicationActions);
1101 this.addLocalTargetActions_(applicationActions);
1102 var applicationActionTitles = new Array();
1103 for (var i = 0, action; action = applicationActions[i]; i++) {
1104 applicationActionTitles.push(action.title);
1105 }
1106 this.pk_.addCompletionListByName(cvox.AxsNav.str.CATEGORY_APPLICATION_ACTIONS,
1107 applicationActionTitles, cvox.AxsNav.str.CATEGORY_APPLICATION_ACTIONS);
1108
1109 // earcons category
1110 if (this.snd_) {
1111 var earconActions = new Array();
1112 this.addEarconActions_(earconActions);
1113 var earconActionTitles = new Array();
1114 for (var i = 0, action; action = earconActions[i]; i++) {
1115 earconActionTitles.push(action.title);
1116 }
1117 var self = this;
1118 this.pk_.setBrowseCallback(function(completion) {
1119 if (this.managedCompletionListName == cvox.AxsNav.str.CATEGORY_EARCONS) {
1120 var earcon = self.titleToEarconMap[completion];
1121 self.snd_.play(earcon);
1122 }
1123 });
1124 this.pk_.addCompletionListByName(cvox.AxsNav.str.CATEGORY_EARCONS,
1125 earconActionTitles, cvox.AxsNav.str.CATEGORY_EARCONS);
1126 }
1127
1128 // show PowerKey
1129 this.pk_.setBrowseOnly(true);
1130 this.pk_.setCompletionListByName(cvox.AxsNav.str.CATEGORY_HELP_INSTRUCTIONS);
1131 this.pk_.updateCompletionField(PowerKey.status.VISIBLE, true, 40, 20);
1132 };
1133
1134 /**
1135 * Returns the first found function mapped to a hot key
1136 * in the given hot key string.
1137 *
1138 * @param {string} hotkeyStr
1139 * The hot key string.
1140 * @return {Function?} The first function mapped to a hot key.
1141 * @private
1142 */
1143 cvox.AxsNav.prototype.getFunctionForHotkeyStr_ = function(hotkeyStr) {
1144 var keys = hotkeyStr.split(' ');
1145 for (var i = 0, key; key = keys[i]; i++) {
1146 var keyCode = -1;
1147 var charCode = -1;
1148 if (key == 'LEFT') {
1149 keyCode = 37;
1150 } else if (key == 'UP') {
1151 keyCode = 38;
1152 } else if (key == 'RIGHT') {
1153 keyCode = 39;
1154 } else if (key == 'DOWN') {
1155 keyCode = 40;
1156 } else if (key == 'PGUP') {
1157 keyCode = 33;
1158 } else if (key == 'PGDOWN') {
1159 keyCode = 34;
1160 } else if (key == 'ENTER') {
1161 keyCode = 13;
1162 } else if (key == 'DEL') {
1163 keyCode = 46;
1164 } else if (key == 'ESC') {
1165 keyCode = 27;
1166 } else {
1167 charCode = key.charCodeAt(0);
1168 }
1169 var command = this.getFunctionForKey(keyCode, charCode);
1170 if (command) {
1171 return command;
1172 }
1173 }
1174 return null;
1175 };
1176
1177 /**
1178 * Gets the global available actions in the current context. Each action has a
1179 * "title" member and a "keys" member.
1180 *
1181 * @return {Array}
1182 * An array of the globally available actions.
1183 */
1184 cvox.AxsNav.prototype.getGlobalActions = function() {
1185 var globalActions = new Array();
1186 this.addGlobalNavigationActions_(globalActions);
1187 this.addGlobalListNavigationActions_(globalActions);
1188 this.addGlobalTargetActions_(globalActions);
1189 return globalActions;
1190 };
1191
1192 /**
1193 * Add the global navigation actions.
1194 *
1195 * @param {Array} actions An array to which to add the actions.
1196 *
1197 * @private
1198 */
1199 cvox.AxsNav.prototype.addGlobalNavigationActions_ = function(actions) {
1200 // next list
1201 this.addNewAction_(actions, cvox.AxsNav.str.NEXT_LIST, this.nextListKeys);
1202 // previous list
1203 this.addNewAction_(actions, cvox.AxsNav.str.PREV_LIST, this.prevListKeys);
1204
1205 };
1206
1207 /**
1208 * Add the global navigation actions for going to a list.
1209 *
1210 * @param {Array} actions An array to which to add the actions.
1211 *
1212 * @private
1213 */
1214 cvox.AxsNav.prototype.addGlobalListNavigationActions_ = function(actions) {
1215 for (var i = 0, list; list = this.navArray[i]; i++) {
1216 this.addNewAction_(actions, list.title, list.hotKeys);
1217 }
1218 };
1219
1220 /**
1221 * Add the global target actions.
1222 *
1223 * @param {Array}
1224 * actions An array to which to add the actions.
1225 * @private
1226 */
1227 cvox.AxsNav.prototype.addGlobalTargetActions_ = function(actions) {
1228 for (var j = 0, target; target = this.targetsArray[j]; j++) {
1229 if (this.isValidTargetAction(target)) {
1230 this.addNewAction_(actions, target.title, target.hotkey);
1231 }
1232 }
1233 };
1234
1235 /**
1236 * Gets the locally available actions in the current context. Each action has a
1237 * "title" member and a "keys" member.
1238 *
1239 * @return {Array}
1240 * An array of the locally available actions.
1241 */
1242 cvox.AxsNav.prototype.getLocalActions = function() {
1243 var localActions = new Array();
1244 this.addLocalNavigationActions_(localActions);
1245 this.addLocalTargetActions_(localActions);
1246 return localActions;
1247 };
1248
1249 /**
1250 * Add the local navigation actions.
1251 *
1252 * @param {Array}
1253 * actions An array to which to add the actions.
1254 * @private
1255 */
1256 cvox.AxsNav.prototype.addLocalNavigationActions_ = function(actions) {
1257 var list = this.currentList();
1258 if (!list) {
1259 return;
1260 }
1261 // next item
1262 this.addNewAction_(actions, cvox.AxsNav.str.CYCLE_NEXT_ITEM, list.nextKeys);
1263 // previous item
1264 this.addNewAction_(actions, cvox.AxsNav.str.CYCLE_PREV_ITEM, list.prevKeys);
1265 // forward
1266 this.addNewAction_(actions, cvox.AxsNav.str.FORWARD_ITEM, list.fwdKeys);
1267 // backward
1268 this.addNewAction_(actions, cvox.AxsNav.str.BACKWARDS_ITEM, list.backKeys);
1269 };
1270
1271 /**
1272 * Add the local target actions.
1273 *
1274 * @param {Array}
1275 * actions An array to which to add the actions.
1276 * @private
1277 */
1278 cvox.AxsNav.prototype.addLocalTargetActions_ = function(actions) {
1279 var targets = this.currentList().targets;
1280 for (var i = 0, target; target = targets[i]; i++) {
1281 if (this.isValidTargetAction(target)) {
1282 this.addNewAction_(actions, target.title, target.hotkey);
1283 }
1284 }
1285 };
1286
1287 /**
1288 * Add the earcon actions.
1289 *
1290 * @param {Array}
1291 * actions An array to which to add the actions.
1292 * @private
1293 */
1294 cvox.AxsNav.prototype.addEarconActions_ = function(actions) {
1295 // list
1296 this.addNewAction_(actions, cvox.AxsNav.str.CATEGORY_EARCONS_LIST, null);
1297 // item
1298 this.addNewAction_(actions, cvox.AxsNav.str.CATEGORY_EARCONS_ITEM, null);
1299 // wrap
1300 this.addNewAction_(actions, cvox.AxsNav.str.CATEGORY_EARCONS_WRAP, null);
1301 };
1302
1303 /**
1304 * Creates a PowerKey action and adds it at the end of the
1305 * given actions array.
1306 *
1307 * @param {Array}
1308 * actions The actions array to add.
1309 * @param {Stirng}
1310 * title The action title.
1311 * @param {Stirng}
1312 * keys The action key bindings.
1313 * @private
1314 */
1315 cvox.AxsNav.prototype.addNewAction_ = function(actions, title, keys) {
1316 var action = new Object();
1317 action.title = title;
1318 if (keys && keys !== '') {
1319 action.title = action.title + ', ' +
1320 keys.toLowerCase().replace(' ', ' ' + cvox.AxsNav.str.OR + ' ');
1321 action.keys = keys;
1322 }
1323 actions.push(action);
1324 };
1325
1326 /**
1327 * Initializes the PowerKey instance that presents the valid actions. This
1328 * method initializes the PowerKey object with reasonable default values.
1329 */
1330 cvox.AxsNav.prototype.defaultInitPowerKeyObj = function() {
1331 var parentElement = this.axs_.getActiveDocument().body;
1332 this.pk_.createCompletionField(parentElement, 50, null, null, null, false);
1333 this.pk_.setCompletionPromptStr(cvox.AxsNav.str.SELECT_ACTION);
1334 this.pk_.setAutoHideCompletionField(true);
1335 this.pk_.setDefaultCSSStyle();
1336 this.pk_.setManagedCompletionListCallback(function(name) {
1337 this.managedCompletionListName = name;
1338 });
1339 };
1340
1341 /**
1342 * Returns true if the xPath of this target. evaluates to a non empty set of
1343 * nodes.
1344 *
1345 * @param {Object}
1346 * target A target object.
1347 * @return {boolean} Whether the target action is valid.
1348 */
1349 cvox.AxsNav.prototype.isValidTargetAction = function(target) {
1350 var valid = false;
1351
1352 if (target.hotkey !== '') {
1353 var xPath = target.xpath;
1354 var rootNode = this.axs_.getActiveDocument().body;
1355
1356 // Handle relative XPath
1357 if (xPath.indexOf('.') === 0) {
1358 var currentItem = this.currentItem();
1359 if (currentItem) {
1360 rootNode = currentItem.elem;
1361 }
1362 }
1363
1364 // Find xPaths that return non empty set of nodes
1365 var nodes = this.axs_.evalXPath(xPath, rootNode);
1366 if (nodes.length > 0) {
1367 valid = true;
1368 }
1369 }
1370 return valid;
1371 };
1372
1373 /**
1374 * Builds up the navigation system of lists of items. This system uses the idea
1375 * of multiple cursors and the visitor pattern.
1376 *
1377 * @param {Object}
1378 * cnrJson The CNR as a JSON.
1379 *
1380 * @notypecheck {Function?} opt_customNavMethod.
1381 *
1382 * @param {Function?}
1383 * opt_customNavMethod A custom navigation method provided by the
1384 * caller. This navigation method will be given the original cnrJson,
1385 * the navigation array of lists of items, an array of all the lists
1386 * which had zero items, and an array of targets. If this is null,
1387 * the default AxsJAX nav handler will be used.
1388 */
1389 cvox.AxsNav.prototype.navInitJson = function(cnrJson, opt_customNavMethod) {
1390 this.navArray = new Array();
1391 this.navListIdx = 0;
1392 this.navItemIdxs = new Array();
1393
1394 var emptyLists = new Array();
1395
1396 var i;
1397 var currentList;
1398 for (i = 0, currentList; currentList = cnrJson.lists[i]; i++) {
1399 var navList = new Object();
1400 currentList.listId = i; // create an id for the list
1401 navList.cnrNode = null;
1402 navList.origListObj = currentList;
1403 navList.title = currentList.title || '';
1404 navList.hotKeys = currentList.hotkey || '';
1405 navList.nextKeys = currentList.next || '';
1406 navList.prevKeys = currentList.prev || '';
1407 navList.fwdKeys = currentList.fwd || '';
1408 navList.backKeys = currentList.back || '';
1409 navList.onEmpty = currentList.onEmpty || '';
1410 navList.type = currentList.type || '';
1411 navList.tailTarget = null;
1412 navList.headTarget = null;
1413 navList.entryTarget = null;
1414 navList.items = this.makeItemsArray(currentList);
1415 navList.targets = this.makeTargetsArray(currentList);
1416 for (var j = 0, listTarget; listTarget = navList.targets[j]; j++) {
1417 if (listTarget.trigger == 'listTail') {
1418 navList.tailTarget = listTarget;
1419 } else if (listTarget.trigger == 'listHead') {
1420 navList.headTarget = listTarget;
1421 } else if (listTarget.trigger == 'listEntry') {
1422 navList.entryTarget = listTarget;
1423 }
1424 }
1425 if (navList.items.length > 0 || navList.type == 'dynamic') {
1426 // Only add nav lists that have content to the array
1427 this.navArray.push(navList);
1428 this.navItemIdxs.push(-1);
1429 } else if (navList.hotKeys !== '') {
1430 // Record empty nav lists if the user can jump to them directly
1431 emptyLists.push(navList);
1432 }
1433 }
1434
1435 // Build up the targets
1436 var targets = new Array();
1437 this.targetsArray = new Array();
1438 this.targetsIdx = 0;
1439 var currentTarget;
1440 if (cnrJson.targets) {
1441 for (i = 0, currentTarget; currentTarget = cnrJson.targets[i]; i++) {
1442 var target = new Object();
1443 // Strip all leading and trailing spaces from the xpath
1444 target.xpath = currentTarget.xpath;
1445 target.xpath = target.xpath.replace(/^\s\s*/, '').replace(/\s\s*$/,
1446 '');
1447 target.title = currentTarget.title || '';
1448 target.trigger = currentTarget.trigger || 'key';
1449 target.hotkey = currentTarget.hotkey || '';
1450 target.action = currentTarget.action || 'click';
1451 target.onEmpty = currentTarget.onEmpty || '';
1452 this.targetsArray.push(target);
1453 }
1454 }
1455
1456 // Remove the previous event listener if there was one
1457 if (this.keyHandler !== null) {
1458 document.removeEventListener('keypress', this.keyHandler, true);
1459 }
1460 // Bind lists and targets to keys if there is no custom handler specified
1461 if ((opt_customNavMethod === null) ||
1462 (typeof (opt_customNavMethod) == 'undefined')) {
1463 this.setUpNavKeys(cnrJson, emptyLists);
1464 } else {
1465 opt_customNavMethod(cnrJson, this.navArray, emptyLists,
1466 this.targetsArray);
1467 }
1468 };
1469
1470 /**
1471 * Makes an array of items given a navigation list node and its index. Elements
1472 * associated with a list will be marked as such.
1473 *
1474 * @param {Object}
1475 * jsonListObj The navigation list node.
1476 * @return {Array} The array of items.
1477 */
1478 cvox.AxsNav.prototype.makeItemsArray = function(jsonListObj) {
1479 var itemsArray = new Array();
1480 if (!jsonListObj.items) {
1481 return itemsArray;
1482 }
1483 for (var i = 0, entry; entry = jsonListObj.items[i]; i++) {
1484 // Do this in a try-catch block since there are multiple
1485 // sets of items and even if one set does not exist as expected,
1486 // the other sets should still be available.
1487 try {
1488 // Strip all leading and trailing spaces from the xpath
1489 var xpath = entry.xpath.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
1490 var htmlElem = this.axs_.getActiveDocument().documentElement;
1491 var elems = this.axs_.evalXPath(xpath, htmlElem);
1492 var idxStr = entry.index || '0';
1493 var idx = parseInt(idxStr, 10);
1494 var count = elems.length - idx;
1495 var countStr = entry.count || '*';
1496 if (countStr != '*') {
1497 count = parseInt(countStr, 10);
1498 }
1499 var end = count + idx;
1500 var action = entry.action || null;
1501 while (idx < end) {
1502 var item = new Object();
1503 item.action = action;
1504 item.elem = elems[idx];
1505 if (typeof (item.elem) != 'undefined') {
1506 if (typeof (item.elem.AxsNavInfo) == 'undefined') {
1507 item.elem.AxsNavInfo = new Object();
1508 }
1509 if (typeof (jsonListObj.listId) == 'undefined') {
1510 throw new Error('list does not have an id');
1511 }
1512 item.elem.AxsNavInfo[jsonListObj.listId] =
1513 itemsArray.length;
1514 itemsArray.push(item);
1515 }
1516 idx++;
1517 }
1518 } catch (err) {
1519 }
1520 }
1521 return itemsArray;
1522 };
1523
1524 /**
1525 * Returns an array of target objects for the given <list> node.
1526 *
1527 * @param {Object}
1528 * jsonListObj A <list> node.
1529 * @return {Array} An array of target objects.
1530 */
1531 cvox.AxsNav.prototype.makeTargetsArray = function(jsonListObj) {
1532 var targetsArray = new Array();
1533 if (!jsonListObj.targets) {
1534 return targetsArray;
1535 }
1536 for (var i = 0, entry; entry = jsonListObj.targets[i]; i++) {
1537 var target = new Object();
1538 // Strip all leading and trailing spaces from the xpath
1539 target.xpath = entry.xpath.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
1540 target.title = entry.title || '';
1541 target.trigger = entry.trigger || 'key';
1542 target.hotkey = entry.hotkey || '';
1543 target.action = entry.action || 'click';
1544 target.onEmpty = entry.onEmpty || '';
1545 targetsArray.push(target);
1546 }
1547 return targetsArray;
1548 };
1549
1550 /**
1551 * This function attaches the default AxsJAX key handler for navigation.
1552 *
1553 * @param {Object}
1554 * cnrJson The CNR as a JSON.
1555 * @param {Array}
1556 * emptyLists An array of lists which have zero items.
1557 */
1558 cvox.AxsNav.prototype.setUpNavKeys = function(cnrJson, emptyLists) {
1559 var self = this;
1560 var i;
1561
1562 this.topCharCodeMap = new Object();
1563 this.topKeyCodeMap = new Object();
1564 this.charCodeMaps = new Array();
1565 this.keyCodeMaps = new Array();
1566
1567 // ? for help
1568 var test = new Array();
1569 test.push('?');
1570 this.assignKeysToMethod(test, this.topCharCodeMap, this.topKeyCodeMap,
1571 function() {
1572 self.showAvailableActionsHelp();
1573 });
1574
1575 // Acting on global targets
1576 var target;
1577 for (i = 0, target; target = this.targetsArray[i]; i++) {
1578 this.assignTargetKeys(target, this.topCharCodeMap, this.topKeyCodeMap);
1579 }
1580
1581 // Moving through lists
1582 var keys = new Array();
1583 this.nextListKeys = cnrJson.next || '';
1584 if (this.nextListKeys !== '') {
1585 keys = this.nextListKeys.split(' ');
1586 }
1587 this.assignKeysToMethod(keys, this.topCharCodeMap, this.topKeyCodeMap,
1588 function() {
1589 self.nextList();
1590 self.doListEntryActions();
1591 });
1592
1593 keys = new Array();
1594 this.prevListKeys = cnrJson.prev || '';
1595 if (this.prevListKeys !== '') {
1596 keys = this.prevListKeys.split(' ');
1597 }
1598 this.assignKeysToMethod(keys, this.topCharCodeMap, this.topKeyCodeMap,
1599 function() {
1600 self.prevList();
1601 self.doListEntryActions();
1602 });
1603
1604 // Moving through items and handling per-list targets
1605 var list;
1606 for (i = 0, list; list = this.navArray[i]; i++) {
1607 var charMap = new Object();
1608 var keyMap = new Object();
1609 this.charCodeMaps.push(charMap);
1610 this.keyCodeMaps.push(keyMap);
1611 this.assignItemKeys(list.nextKeys, i, 'next');
1612 this.assignItemKeys(list.prevKeys, i, 'prev');
1613 this.assignItemKeys(list.fwdKeys, i, 'fwd');
1614 this.assignItemKeys(list.backKeys, i, 'back');
1615 this.assignHotKeys(list.hotKeys, i, list.onEmpty);
1616 var j;
1617 for (j = 0, target; target = list.targets[j]; j++) {
1618 this.assignTargetKeys(target, charMap, keyMap);
1619 }
1620 }
1621
1622 // Dealing with empty lists with hotkeys
1623 var emptyList;
1624 for (i = 0, emptyList; emptyList = emptyLists[i]; i++) {
1625 this.assignEmptyMsgKeys(emptyList.hotKeys, emptyList.onEmpty);
1626 }
1627
1628 this.keyHandler = function(evt) {
1629 // None of these commands involve Ctrl.
1630 // If Ctrl is held, it must be for some AT.
1631 if (evt.ctrlKey)
1632 return true;
1633 if (self.axs_.inputFocused)
1634 return true;
1635
1636 var command = self.getFunctionForKey(evt.keyCode, evt.charCode);
1637
1638 if (command) {
1639 command();
1640 return false;
1641 }
1642
1643 return true;
1644 };
1645
1646 document.addEventListener('keypress', this.keyHandler, true);
1647 };
1648
1649 /**
1650 * Returns the id of a given list
1651 *
1652 * @param {Object}
1653 * list a list.
1654 * @return {Number} the id of the list.
1655 */
1656 cvox.AxsNav.prototype.getListId = function(list) {
1657 return list.origListObj.listId;
1658 };
1659
1660 /**
1661 * Takes the cursor before the beginning of the specified list.
1662 *
1663 * @param {Number}
1664 * listIndex the index of the given list.
1665 */
1666 cvox.AxsNav.prototype.goToListHead = function(listIndex) {
1667 if (this.navArray.length < 1) {
1668 return;
1669 }
1670 // Clear the last item
1671 this.lastItem = null;
1672 // Set the current list
1673 this.navListIdx = listIndex;
1674 // Update the list
1675 this.validateList(this.navArray[listIndex]);
1676 this.doListEntryActions();
1677 // Set the item at the top
1678 this.navItemIdxs[listIndex] = -1;
1679 };
1680
1681 /**
1682 * Takes the cursor to the end of the specified list.
1683 *
1684 * @param {Number}
1685 * listIndex the index of the given list.
1686 */
1687 cvox.AxsNav.prototype.goToListTail = function(listIndex) {
1688 if (this.navArray.length < 1) {
1689 return;
1690 }
1691 this.goToListHead(listIndex);
1692 // The navList object
1693 var list = this.navArray[listIndex];
1694 // Set the item at the bottom
1695 this.navItemIdxs[listIndex] = list.items.length - 1;
1696 };
1697
1698 /**
1699 * Finds a list id given it's name
1700 *
1701 * @param {string}
1702 * listTitle the name of the list.
1703 * @return {number} The index of the list with the given title or -1 if no list
1704 * with the given title exists.
1705 */
1706 cvox.AxsNav.prototype.findListIndexByTitle = function(listTitle) {
1707 for (var i = 0; i < this.navArray.length; i++) {
1708 var list = this.navArray[i];
1709 if (list.title == listTitle) {
1710 break;
1711 }
1712 }
1713 if (i < this.navArray.length) {
1714 return i;
1715 } else {
1716 return -1;
1717 }
1718 };
1719
1720 /**
1721 * Generates a custom walker based on the CNR for the given object.
1722 * This custom walker can be fed into the ChromeVox nav manager so that all of
1723 * the navigation controls can be cleanly integrated.
1724 *
1725 * @return {Object} A custom walker object.
1726 */
1727 cvox.AxsNav.prototype.generateCustomWalker = function() {
1728 var customWalker = new CustomWalker();
1729 var self = this;
1730 customWalker.next = function() {
1731 self.actOnItem(self.nextItem());
1732 return true;
1733 };
1734 customWalker.previous = function() {
1735 self.actOnItem(self.prevItem());
1736 return true;
1737 };
1738 customWalker.goToCurrentItem = function() {
1739 self.actOnItem(self.currentItem());
1740 return true;
1741 };
1742 customWalker.actOnCurrentItem = function() {
1743 var target = self.targetsArray[self.navListIdx];
1744 self.actOnTarget(target);
1745 return true;
1746 };
1747 customWalker.getCurrentNode = function() {
1748 return self.currentItem().elem;
1749 };
1750 customWalker.setCurrentNode = function(targetNode) {
1751 self.goToListHead(self.navListIdx);
1752 var currentItem = self.nextItem();
1753 while (currentItem) {
1754 if (targetNode.compareDocumentPosition(currentItem.elem) ==
1755 Node.DOCUMENT_POSITION_PRECEDING) {
1756 currentItem = self.nextItem();
1757 } else {
1758 return;
1759 }
1760 }
1761 };
1762 return customWalker;
1763 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698