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

Side by Side Diff: chrome/browser/resources/access_chromevox/powerkey-bundle.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 /**
6 * Javascript class for providing keyboard interface enhancements for
7 * Web 2.0 applications.
8 * @param {string} context The user specified string value, which is the
9 * starting context of the application. This can be changed later.
10 * @param {AxsJAX} axsJAX The AxsJAX object provided by the user.
11 * @constructor
12 */
13 var PowerKey = function(context, axsJAX) {
14 /**
15 * Holds the current application context.
16 * @type {string}
17 */
18 this.context = context;
19
20 /**
21 * The div element holding the completion text field.
22 * @type {Element?}
23 */
24 this.cmpFloatElement = null;
25
26 /**
27 * The completion text field element.
28 * @type {Element?}
29 */
30 this.cmpTextElement = null;
31
32 /**
33 * The div element for the page-wide background of the completion field.
34 * @type {Element?}
35 */
36 this.backgroundDivElement = null;
37
38 /**
39 * The div element which holds the text of the selected list item.
40 * @type {Element?}
41 * @private
42 */
43 this.listElement_ = null;
44
45 /**
46 * The div element inside the completion div, holding the selected list item.
47 * @type {Element?}
48 * @private
49 */
50 this.cmpDivElement_ = null;
51
52 /**
53 * Variable to hold the completion mode prompt string.
54 * @type {string}
55 * @private
56 */
57 this.completionPromptStr_ = 'Enter Completion';
58
59 /**
60 * Variable to hold the completion mode prompt string when the completion
61 * list is empty.
62 * @type {string}
63 * @private
64 */
65 this.noCompletionStr_ = 'No completions found';
66
67 /**
68 * AxsJAX object
69 * @type {AxsJAX?}
70 * @private
71 */
72 this.axsJAX_ = null;
73
74 /**
75 * The list of completions to select from.
76 * @type {Array}
77 * @private
78 */
79 this.cmpList_ = [];
80
81 /**
82 * The navigation position in the list.
83 * @type {number}
84 * @private
85 */
86 this.listPos_ = -1;
87
88 /**
89 * A collection of completion lists.
90 * @type {Object}
91 * @private
92 */
93 this.managedCmpLists_ = new Array();
94
95 /**
96 * The current position in the managed completion lists.
97 * @type {number}
98 * @private
99 */
100 this.managedCmpListsPos_ = -1;
101
102 /**
103 * Whether to hide completion field on blur.
104 * @type {boolean}
105 * @private
106 */
107 this.hideCmdFieldOnBlur_ = false;
108
109 /**
110 * Whether to show the page-wide background.
111 * @type {boolean}
112 * @private
113 */
114 this.showBackgroundDiv_ = false;
115
116 /**
117 * Color of the page-wide background.
118 * @type {string}
119 * @private
120 */
121 this.backgroundColor_ = '#000000';
122
123 /**
124 * Transparency of the page-wide background 100 being fully
125 * opaque and 0 being fully transparent.
126 * @type {number}
127 * @private
128 */
129 this.backgroundTransparency_ = 0;
130
131 /**
132 * Function to callback on browsing or filtering.
133 * @type {?function(string, string)}
134 * @private
135 */
136 this.browseCallback_ = null;
137
138 /**
139 * The HashMap which provides a context-based mapping
140 * from keys to functions.
141 * @type {?Object}
142 * @private
143 */
144 this.completionActionMap_ = null;
145
146 /**
147 * The completion handler.
148 * handler = function(completion, index, elementId, args) {}.
149 * @type {?function(string, string, Node, Array)}
150 * @private
151 */
152 this.completionHandler_ = null;
153
154 /**
155 * Function to callback on changing the managed completion list.
156 * @type {?function(string)}
157 * @private
158 */
159 this.managedCompletionListCallback_ = null;
160
161 this.nodeMap = {};
162 this.indexList_ = {};
163 if (axsJAX && PowerKey.isGecko) {
164 this.axsJAX_ = axsJAX;
165 }
166 var self = this;
167 this.attachHandlerAndListen(window, PowerKey.Event.RESIZE,
168 function(evt) {
169 self.onPageResize_.call(self, evt);
170 }, null);
171 };
172
173
174 /**
175 * The reg exp indicating the pattern of the parameter to a completion. It
176 * should start with '<', end wit '>' and contain only characters and numbers.
177 * @type {RegExp}
178 */
179 PowerKey.CMD_PARAM = /^\<[A-Z|a-z|0-9|\-|\_]+\>$/;
180
181
182 /**
183 * The reg exp to check if there are spaces, new lines or carriage returns at
184 * the beginning of a string.
185 * @type {RegExp}
186 */
187 PowerKey.LEFT_TRIMMABLE = /^(\s|\r|\n)+/;
188
189
190 /**
191 * The reg exp to check if there are spaces, new lines or carriage returns at
192 * the end of a string.
193 * @type {RegExp}
194 */
195 PowerKey.RIGHT_TRIMMABLE = /(\s|\r|\n)+$/;
196
197
198 /**
199 * Attaches event listener and sets the user specified handler or the
200 * default handler if the action map is provided.
201 * @param {EventTarget?} target The element to attach the event listerner to.
202 * @param {string} event The event to listen for.
203 * @notypecheck {Function?} handler.
204 * @param {Function?} handler The event handler.
205 * @param {Object?} actionMap The HashMap which provides a context-based
206 * mapping from keys to functions.
207 */
208 PowerKey.prototype.attachHandlerAndListen = function(target,
209 event,
210 handler,
211 actionMap) {
212 // Firefox
213 if (PowerKey.isGecko && handler) {
214 target.addEventListener(event, handler, true);
215 } else if (PowerKey.isIE && handler) { // IE
216 target.attachEvent(event, function(event) {
217 handler(event);});
218 }
219 // Use default handler if the action map is provided.
220 if (actionMap) {
221 actionMap = this.expandActionMap(actionMap);
222 var handlerObj = new PowerKey.DefaultHandler(actionMap);
223 var pkObj = this;
224 this.attachHandlerAndListen(target, event, function(evt) {
225 handlerObj.handler(evt, handlerObj, pkObj);
226 }, null);
227 }
228 };
229
230
231 /**
232 * Expand the action map by splitting multiple keys specified in a single
233 * mapping separated by commas.
234 * @param {Object} map The action map.
235 * @return {Object} Returns the updated action map.
236 */
237 PowerKey.prototype.expandActionMap = function(map) {
238 for (var key in map) {
239 var toks = key.split(',');
240 if (toks.length > 1) {
241 for (var i in toks) {
242 map[toks[i]] = map[key];
243 }
244 map[key] = null;
245 }
246 }
247 return map;
248 };
249
250
251 /**
252 * Detaches event listener with the user specified event and handler.
253 * @param {Element} target The element to detach the event listerner from.
254 * @param {string} event The event to detach.
255 * @param {Function} handler The event handler to stop calling.
256 */
257 PowerKey.prototype.detachHandler = function(target, event, handler) {
258 // Firefox
259 if (PowerKey.isGecko) {
260 target.removeEventListener(event, handler, true);
261 } else if (PowerKey.isIE) { // IE
262 target.detachEvent(event, handler);
263 }
264 };
265
266
267 /**
268 * Creates a floating element for holding the completion shell's text field.
269 * @param {Element} parent The element whose child this element will be.
270 * @param {number} size The size of the completion text field in # of
271 * characters.
272 * @param {Function?} handler The completion handler.
273 * handler = function(completion, index, node, args) {}.
274 * @param {Object?} actionMap The object consisting of completion strings as
275 * keys and functions as values.
276 * @param {Array?} completionList The array of completions.
277 * @param {boolean} browseOnly Whether the completion list is browse-only.
278 */
279 PowerKey.prototype.createCompletionField = function(parent,
280 size,
281 handler,
282 actionMap,
283 completionList,
284 browseOnly) {
285 var self = this;
286 var floatId, fieldId, oldCmdNode, divId, bgDivId;
287 // If the completion field already exists, remove it and create a new one.
288 if (this.cmpFloatElement) {
289 // TODO: remove the handlers attached to cmpTextElement before removing
290 // the completion field. The inline handler needs to be moved outside.
291 this.cmpFloatElement.parentNode.removeChild(this.cmpFloatElement);
292 }
293 do {
294 floatId = 'completionField_' + Math.floor(Math.random() * 1001);
295 fieldId = 'txt_' + floatId;
296 divId = 'div_' + floatId;
297 bgDivId = 'bgdiv_' + floatId;
298 oldCmdNode = document.getElementById(floatId);
299 } while (oldCmdNode);
300
301 if (this.backgroundDivElement) {
302 this.backgroundDivElement.parentNode.removeChild(
303 this.backgroundDivElement);
304 }
305
306 // The background element
307 var bgNode = document.createElement('div');
308 bgNode.id = bgDivId;
309 bgNode.style.display = 'none';
310
311 // create completion field element
312 var cmpNode = document.createElement('div');
313 cmpNode.id = floatId;
314 cmpNode.style.position = 'absolute';
315
316 // text field in which the user types the completion.
317 var txtNode = document.createElement('input');
318 txtNode.type = 'text';
319 txtNode.id = fieldId;
320 txtNode.size = size;
321 txtNode.value = '';
322 txtNode.setAttribute('aria-owns', divId);
323 txtNode.onkeypress =
324 function(evt) {
325 evt.stopPropagation();
326 if (evt.keyCode == PowerKey.keyCodes.TAB) {
327 return false;
328 }
329 };
330 txtNode.readOnly = browseOnly;
331
332 if (browseOnly) {
333 txtNode.style.fontSize = 0;
334 }
335
336 // This div element holds the currently selected completion list item.
337 var divNode = document.createElement('div');
338 divNode.id = divId;
339 divNode.setAttribute('tabindex', 0);
340 divNode.setAttribute('role', 'row');
341
342 cmpNode.appendChild(divNode);
343 cmpNode.appendChild(txtNode);
344 parent.appendChild(bgNode);
345 parent.appendChild(cmpNode);
346
347 this.cmpFloatElement = cmpNode;
348 this.cmpTextElement = txtNode;
349 this.backgroundDivElement = bgNode;
350 this.cmpDivElement_ = divNode;
351 this.listElement_ = null;
352
353 this.cmpFloatElement.className = 'pkHiddenStatus';
354 this.cmpTextElement.className = 'pkOpaqueCompletionText';
355 this.backgroundDivElement.className = 'pkBackgroundHide';
356
357 this.completionActionMap_ = actionMap;
358 this.completionHandler_ = handler;
359
360 // Initialize completion list
361 if (completionList) {
362 this.addCompletionListByName(this.context, completionList,
363 this.completionPromptStr_);
364 this.setCompletionListByName(this.context);
365 }
366
367 // filter the completion list on keyup if it is not UP or DOWN arrow.
368 this.attachHandlerAndListen(this.cmpTextElement, PowerKey.Event.KEYUP,
369 function(evt) {
370 self.handleCompletionKeyUp_.call(self, evt);
371 }, null);
372
373 this.attachHandlerAndListen(this.cmpTextElement, PowerKey.Event.KEYDOWN,
374 function(evt) {
375 self.handleCompletionKeyDown_.call(self, evt);
376 }, null);
377
378 this.attachHandlerAndListen(this.cmpTextElement, PowerKey.Event.BLUR,
379 function(evt) {
380 if (self.hideCmdFieldOnBlur_) {
381 self.updateCompletionField(PowerKey.status.HIDDEN);
382 }
383 }, null);
384 };
385
386
387 /**
388 * Sets if the completion field is browse only.
389 * @param {boolean} browseOnly If the completion field is browse only.
390 */
391 PowerKey.prototype.setBrowseOnly = function(browseOnly) {
392 if (this.cmpTextElement) {
393 this.cmpTextElement.readOnly = browseOnly;
394 }
395 };
396
397
398 /**
399 * Tells whether to hide the completion field when focus is lost.
400 * @param {boolean} hide If true, hide the completion field on blur.
401 */
402 PowerKey.prototype.setAutoHideCompletionField = function(hide) {
403 this.hideCmdFieldOnBlur_ = hide;
404 };
405
406
407 /**
408 * Sets the function to be called back when browsing/filtering the list.
409 * @param {Function} callback The callback function.
410 */
411 PowerKey.prototype.setBrowseCallback = function(callback) {
412 this.browseCallback_ = callback;
413 };
414
415
416 /**
417 * Sets the map which provides a context-based mapping
418 * from completion values (keys) to functions. Note that
419 * setting this map implies use of the default completion
420 * handler and setting you custom one will have no effect.
421 * A mapping is triggered after the user has selected an
422 * action form the auto-completion list via pressing
423 * ENTER.
424 * @param {Object} completionActionMap The map instance.
425 */
426 PowerKey.prototype.setCompletionActionMap = function(completionActionMap) {
427 this.completionActionMap_ = completionActionMap;
428 };
429
430
431 /**
432 * Sets the handler which provides custom actions for completion
433 * values. Note that setting a completion map implies use of the
434 * default completion handler therefore setting the completion
435 * handler will have no effect. The handler signature is as follows:
436 *
437 * handler = function(completion, index, elementId, args) {}.
438 *
439 * @param {Function} completionHandler The map instance.
440 */
441 PowerKey.prototype.setCompletionHandler = function(completionHandler) {
442 this.completionHandler_ = completionHandler;
443 };
444
445
446 /**
447 * Sets the function to be called back when changing the managed
448 * (named) completion list.
449 * @param {Function} callback The callback function.
450 */
451 PowerKey.prototype.setManagedCompletionListCallback = function(callback) {
452 this.managedCompletionListCallback_ = callback;
453 };
454
455
456 /**
457 * Sets the label to be displayed and spoken, when the
458 * completion field is made visible.
459 * @param {string} str The string to display.
460 */
461 PowerKey.prototype.setCompletionPromptStr = function(str) {
462 this.completionPromptStr_ = str;
463 };
464
465
466 /**
467 * Sets the label to be displayed and spoken, when there are no completions
468 * in the completion list.
469 * @param {string} str The string to display.
470 */
471 PowerKey.prototype.setNoCompletionStr = function(str) {
472 this.noCompletionStr_ = str;
473 };
474
475
476 /**
477 * Sets the completion list.
478 * @param {Array} list The array to be used as the completion list.
479 */
480 PowerKey.prototype.setCompletionList = function(list) {
481 if (!list) {
482 return;
483 }
484 this.managedCmpListsPos_ = -1;
485 this.cmpList_ = list;
486 this.filterList_ = this.cmpList_;
487 this.indexList_ = {};
488 for (var i = 0, cmp; cmp = this.cmpList_[i]; i++) {
489 this.indexList_[cmp.toLowerCase()] = i;
490 }
491 this.listPos_ = -1;
492 };
493
494
495 /**
496 * Adds a completion list. A completion list is identified by its
497 * name. If a completion list by that name already exists,
498 * then the Ids of the strings in the existing list are removed.
499 * @param {string} name The name of the completion list.
500 * @param {Array} list The array to be used as the completion list.
501 * @param {string} prompt The prompt for this completion list.
502 */
503 PowerKey.prototype.addCompletionListByName = function(name, list, prompt) {
504 var oldManagedCmpList = this.getManagedCompletionListByName_(name);
505 if (oldManagedCmpList) {
506 for (var i = 0, item; item = oldManagedCmpList.values[i]; i++) {
507 this.nodeMap[item] = null;
508 }
509 oldManagedCmpList.list = list;
510 oldManagedCmpList.completionPromptStr = prompt;
511 } else {
512 var newManagedCmpList = new Object();
513 newManagedCmpList.values = list;
514 newManagedCmpList.name = name;
515 newManagedCmpList.completionPromptStr = prompt;
516 newManagedCmpList.index = this.managedCmpLists_.length;
517 this.managedCmpLists_.push(newManagedCmpList);
518 }
519 };
520
521
522 /**
523 * Sets the completion list.
524 * @param {string} name The name of the list to be used for completion.
525 */
526 PowerKey.prototype.setCompletionListByName = function(name) {
527 var cmpList = this.getManagedCompletionListByName_(name);
528 if (!cmpList) {
529 return;
530 }
531 this.setManagedCompletionList_(cmpList.index);
532 };
533
534
535 /**
536 * Gets a completion list given its name.
537 * @param {string} name The name of the completion list.
538 * @return {Object?} The list with the given name.
539 * @private
540 */
541 PowerKey.prototype.getManagedCompletionListByName_ = function(name) {
542 for (var i = 0, list; list = this.managedCmpLists_[i]; i++) {
543 if (list.name == name) {
544 return list;
545 }
546 }
547 return null;
548 };
549
550
551 /**
552 * Sets the color and transparency of the background.
553 * @param {string?} listName Name of the completion list for which to set
554 * the background property.
555 * @param {boolean} show Whether to show the background div when completion
556 * field is made visible.
557 * @param {string?} color The color of the background.
558 * @param {number?} transparency The transparency level, 100 being fully
559 * opaque and 0 being fully transparent.
560 */
561 PowerKey.prototype.setBackgroundStyle = function(listName,
562 show,
563 color,
564 transparency) {
565 if (listName) {
566 var managedCmpList = this.getManagedCompletionListByName_(listName);
567 if (managedCmpList) {
568 managedCmpList.backgroundShow = show;
569 managedCmpList.backgroundColor = color;
570 managedCmpList.backgroundTransparency = transparency;
571 }
572 } else {
573 this.showBackgroundDiv_ = show;
574 if (color) {
575 this.backgroundColor_ = color;
576 }
577 if (transparency) {
578 this.backgroundTransparency_ = transparency;
579 }
580 }
581 };
582
583
584 /**
585 * Sets the style of the completion field.
586 * @param {string} textColor Color of the completion text.
587 * @param {number} textSize Size of the completion text.
588 * @param {string} bgColor Background color of the completion field.
589 * @param {number} transparency Background tranparency, 100 being fully
590 * opaque and 0 being fully transparent.
591 * @param {string} fontStyle Font style of the completion text.
592 * @param {string} fontFamily Font family of the completion text.
593 */
594 PowerKey.prototype.setCompletionFieldStyle = function(textColor,
595 textSize,
596 bgColor,
597 transparency,
598 fontStyle,
599 fontFamily) {
600 if (textSize) {
601 this.cmpFloatElement.style.fontSize = textSize + 'px';
602 this.cmpTextElement.style.fontSize = textSize + 'px';
603 this.cmpTextElement.style.height = (textSize + 5) + 'px';
604 }
605 if (textColor) {
606 this.cmpFloatElement.style.color = textColor;
607 this.cmpTextElement.style.color = textColor;
608 }
609 if (bgColor) {
610 this.cmpFloatElement.style.backgroundColor = bgColor;
611 this.cmpTextElement.style.backgroundColor = bgColor;
612 }
613 if (transparency) {
614 this.cmpFloatElement.style.setProperty('-moz-opacity',
615 '' + (transparency / 100), '');
616 }
617 if (fontStyle) {
618 this.cmpFloatElement.style.fontStyle = fontStyle;
619 this.cmpTextElement.style.fontStyle = fontStyle;
620 }
621 if (fontFamily) {
622 this.cmpFloatElement.style.fontFamily = fontFamily;
623 this.cmpTextElement.style.fontFamily = fontFamily;
624 }
625 };
626
627
628 /**
629 * Updates the completion field element with the new visibility status
630 * and location parameters.
631 * @param {string} status Indicates whether the completion field should be
632 * made PowerKey.status.VISIBLE or PowerKey.status.HIDDEN.
633 * @param {boolean} opt_resize Indicates whether resizing is necessary.
634 * @param {number} opt_top The y-coordinate pixel location of the top
635 * border of the element.
636 * @param {number} opt_left The x-coordinate pixel location of the left
637 * border of the element.
638 */
639 PowerKey.prototype.updateCompletionField = function(status,
640 opt_resize,
641 opt_top,
642 opt_left) {
643 if (!this.cmpFloatElement) {
644 return;
645 }
646 var backgoundShow = this.showBackgroundDiv_;
647 var backgroundColor = this.backgroundColor_;
648 var backgroundTransparency = this.backgroundTransparency_;
649 var managedCmpList = undefined;
650 if (this.managedCmpListsPos_ > -1) {
651 managedCmpList = this.managedCmpLists_[this.managedCmpListsPos_];
652 if (managedCmpList.backgoundShow) {
653 backgoundShow = managedCmpList.backgoundShow;
654 }
655 if (managedCmpList.backgroundColor) {
656 backgroundColor = managedCmpList.backgroundColor;
657 }
658 if (managedCmpList.backgroundTransparency) {
659 managedCmpList.backgroundTransparency = backgroundTransparency;
660 }
661 }
662 if (status == PowerKey.status.VISIBLE) {
663 if (this.cmpFloatElement.className == 'pkHiddenStatus' ||
664 this.listPos_ < 0) {
665 if (managedCmpList) {
666 this.setListElement_(managedCmpList.completionPromptStr);
667 } else {
668 this.setListElement_(this.completionPromptStr_);
669 }
670 }
671 this.showBackground_(backgoundShow, backgroundColor,
672 backgroundTransparency);
673 this.cmpFloatElement.className = 'pkVisibleStatus';
674 // Need to do this for IE. Setting focus immediately after making it
675 // visible generates an error. Hence have to set the timeout.
676 var elem = this.cmpTextElement;
677 window.setTimeout(function() {elem.focus();}, 0);
678 } else if (status == PowerKey.status.HIDDEN) {
679 if (PowerKey.isIE && this.listElement_) {
680 this.listElement_.innerText = '';
681 } else if (this.listElement_) {
682 this.listElement_.textContent = '';
683 }
684 this.showBackground_(false, backgroundColor, backgroundTransparency);
685 this.cmpFloatElement.className = 'pkHiddenStatus';
686 this.cmpTextElement.value = '';
687 this.listPos_ = -1;
688 }
689 if (opt_resize) {
690 var viewportSz = PowerKey.getViewportSize();
691 if (!opt_top) {
692 opt_top = viewportSz.height - this.cmpFloatElement.offsetHeight;
693 }
694 if (!opt_left) {
695 opt_left = 0;
696 }
697 this.cmpFloatElement.style.top = opt_top;
698 this.cmpFloatElement.style.left = opt_left;
699 }
700 };
701
702
703 /**
704 * Creates the list of completions from the text content of the elements
705 * obtained from the xpath which satisfy the function func.
706 * @param {string} tags The tags to be selected.
707 * @param {Function} func Only those elements are considered for which
708 * this function returns true.
709 * @param {Function?} getText Function to get text for this list element.
710 * @param {boolean} newList If this is true, all entries in idMap
711 * are erased, and a new mapping of completions and IDs is created.
712 * @return {Array} The array of completion strings.
713 */
714 PowerKey.prototype.createCompletionList = function(tags,
715 func,
716 getText,
717 newList) {
718 var cmpList = new Array();
719 var tagArray = tags.split(/\s+/);
720 if (newList) {
721 delete this.nodeMap;
722 this.nodeMap = new Object();
723 }
724 for (var j = 0, tag; tag = tagArray[j]; j++) {
725 var nodeArray = document.getElementsByTagName(tag);
726 for (var i = 0, node; node = nodeArray[i]; i++) {
727 if (func(node)) {
728 if (getText) {
729 var label = getText(node);
730 } else {
731 label = PowerKey.isIE ?
732 node.innerText : node.textContent;
733 }
734 if (label) {
735 label = PowerKey.rightTrim(PowerKey.leftTrim(label));
736 label = label.replace(/\n/g, '');
737 if (label.toLowerCase().indexOf('ctrl+') === 0) {
738 label = label.substring(6);
739 }
740 cmpList.push(label);
741 if (String(this.nodeMap[label.toLowerCase()]) == 'undefined') {
742 this.nodeMap[label.toLowerCase()] = node;
743 }
744 }
745 }
746 }
747 }
748 return cmpList;
749 };
750
751
752 /**
753 * Resizes the background upon page resize if the background is
754 * currently showing.
755 * @param {Event} evt The resize event.
756 * @private
757 */
758 PowerKey.prototype.onPageResize_ = function(evt) {
759 // wait for the resize to complete so that the new
760 // viewport size is available.
761 var self = this;
762 window.setTimeout(function() {
763 if (self.showBackgroundDiv_ &&
764 self.backgroundDivElement.style.display == 'block') {
765 self.showBackground_(self.showBackgroundDiv_, self.backgroundColor_,
766 self.backgroundTransparency_);
767 }
768 }, 0);
769 };
770
771
772 /**
773 * Handle keyup events. If the key is not an arrow key, filter the list by the
774 * contents of the completion text field.
775 * @param {Object} evt The key event object.
776 * @private
777 */
778 PowerKey.prototype.handleCompletionKeyUp_ = function(evt) {
779 if (this.cmpTextElement.value.length === 0) {
780 this.filterList_ = this.cmpList_;
781 }
782 if (evt.keyCode != PowerKey.keyCodes.ARROWUP &&
783 evt.keyCode != PowerKey.keyCodes.ARROWDOWN &&
784 evt.keyCode != PowerKey.keyCodes.ARROWLEFT &&
785 evt.keyCode != PowerKey.keyCodes.ARROWRIGHT &&
786 evt.keyCode != PowerKey.keyCodes.ENTER &&
787 evt.keyCode != PowerKey.keyCodes.TAB &&
788 evt.keyCode != PowerKey.keyCodes.ESC) {
789
790 if (this.cmpTextElement.value.length) {
791 this.filterList_ = this.getWordFilterMatches_(this.cmpList_,
792 this.cmpTextElement.value, 50);
793 this.listPos_ = -1;
794 if (this.filterList_.length > 0) {
795 this.setListElement_(this.filterList_[0]);
796 this.listPos_ = 0;
797 } else {
798 this.setListElement_(this.noCompletionStr_);
799 }
800 } else {
801 if (this.managedCmpListsPos_ > -1) {
802 var managedCmpList = this.managedCmpLists_[this.managedCmpListsPos_];
803 this.setListElement_(managedCmpList.completionPromptStr);
804 } else {
805 this.setListElement_(this.completionPromptStr_);
806 }
807 }
808 }
809 // Handle ENTER key pressed in the completion field.
810 if (evt.keyCode == PowerKey.keyCodes.ENTER) {
811 if (this.cmpTextElement.readOnly) {
812 return;
813 }
814 // Select current filtered list item.
815 if (this.filterList_ &&
816 this.filterList_.length > 0 &&
817 this.filterList_[this.listPos_] &&
818 this.cmpTextElement.value != this.filterList_[this.listPos_] &&
819 // Does not have parameters or is incomplete
820 (this.filterList_[this.listPos_].indexOf('<') < 0 ||
821 (this.filterList_[this.listPos_].indexOf('<') >= 0 &&
822 this.filterList_[this.listPos_].split(' ').length >
823 this.cmpTextElement.value.split(' ').length))) {
824 this.selectCurrentListItem_();
825 }
826 var str = this.cmpTextElement.value;
827 var originalCmd = (PowerKey.isIE ? this.listElement_.innerText :
828 this.listElement_.textContent).toLowerCase();
829 // Change only the basic portion (non-argument) of the selection to lower
830 // case. For ex: In 'Watch Video funnySeries', only 'Watch Video' is
831 // changed to lower case.
832 var pos = originalCmd.indexOf('<');
833 var baseCmd;
834 if (pos >= 0) {
835 baseCmd = str.substr(0, pos - 1).toLowerCase();
836 str = baseCmd + ' ' + str.substr(pos);
837 } else {
838 str = str.toLowerCase();
839 baseCmd = str;
840 }
841 var handled = false;
842 if (this.completionActionMap_) {
843 handled = this.actionHandler_.call(this, str,
844 originalCmd, this.completionActionMap_);
845 }
846 if (this.completionHandler_ && !handled) {
847 var args = this.getArguments_(str, originalCmd);
848 this.completionHandler_(baseCmd, this.indexList_[originalCmd],
849 this.nodeMap[originalCmd], args);
850 }
851 this.cmpTextElement.value = '';
852 } else if (evt.keyCode == PowerKey.keyCodes.ESC) {
853 this.cmpTextElement.blur();
854 this.updateCompletionField(PowerKey.status.HIDDEN);
855 }
856 };
857
858
859 /**
860 * Handle keydown events. If the key is an UP/DOWN arrow key, navigates to the
861 * previous and next item in the filtered list.
862 * @param {Object} evt The key event object.
863 * @private
864 */
865 PowerKey.prototype.handleCompletionKeyDown_ = function(evt) {
866 // Handle UP arrow key
867 if (evt.keyCode == PowerKey.keyCodes.ARROWUP &&
868 this.filterList_ &&
869 this.filterList_.length > 0) {
870 this.prevListItem_();
871 }
872 // Handle DOWN arrow key
873 else if (evt.keyCode == PowerKey.keyCodes.ARROWDOWN &&
874 this.filterList_ &&
875 this.filterList_.length > 0) {
876 this.nextListItem_();
877 if (evt.preventDefault) {
878 evt.preventDefault();
879 }
880 }
881 // Handle LEFT arrow key
882 if (evt.keyCode == PowerKey.keyCodes.ARROWLEFT &&
883 this.cmpTextElement.readOnly &&
884 this.managedCmpLists_.length > 0) {
885 this.prevManagedCompletionList_();
886 }
887 // Handle RIGHT arrow key
888 else if (evt.keyCode == PowerKey.keyCodes.ARROWRIGHT &&
889 this.cmpTextElement.readOnly &&
890 this.managedCmpLists_.length > 0) {
891 this.nextManagedCompletionList_();
892 }
893 // On TAB, keep the focus in the completion field.
894 else if (evt.keyCode == PowerKey.keyCodes.TAB) {
895 if (this.filterList_ && this.filterList_.length > 0) {
896 this.selectCurrentListItem_();
897 }
898 if (evt.preventDefault) {
899 evt.preventDefault();
900 }
901 }
902 };
903
904
905 /**
906 * Shows or hides the page-wide background div. Used internally by
907 * updateCompletionField().
908 * @param {boolean} show Whether to show the background or not.
909 * @param {string} color The color of the background.
910 * @param {number} transparency The transparency level, 100 being fully opaque
911 * and 0 being fully transparent.
912 * @private
913 */
914 PowerKey.prototype.showBackground_ = function(show, color, transparency) {
915 if (show) {
916 this.backgroundDivElement.className = 'pkBackgroundShow';
917 this.backgroundDivElement.style.display = 'block';
918 var viewportSz = PowerKey.getViewportSize();
919 this.backgroundDivElement.style.width = viewportSz.width + 'px';
920 this.backgroundDivElement.style.height = viewportSz.height + 'px';
921 if (color) {
922 this.backgroundDivElement.style.backgroundColor = color;
923 }
924 if (transparency) {
925 this.backgroundDivElement.style.setProperty('-moz-opacity',
926 '' + (transparency / 100), '');
927 }
928 } else {
929 this.backgroundDivElement.style.display = 'none';
930 }
931 };
932
933
934 /**
935 * Splits token into words and filters the rows by these words.
936 * @param {Array} list An array of all completions.
937 * @param {string} token Token to match.
938 * @param {number} maxMatches Max number of matches to return.
939 * @return {Array} matches Returns the array of matching rows.
940 * @private
941 */
942 PowerKey.prototype.getWordFilterMatches_ = function(list, token, maxMatches) {
943 var matches = list;
944 var rows = list;
945 var words = token.split(' ');
946 for (var i = 0, word; word = words[i]; i++) {
947 rows = matches;
948 matches = [];
949 if (word !== '') {
950 var escapedToken = PowerKey.regExpEscape(word);
951 var matcher = new RegExp('(^|\\W+)' + escapedToken, 'i');
952 for (var j = 0, row; row = rows[j]; j++) {
953 if (String(row).match(matcher)) {
954 matches.push(row);
955 }
956 }
957 }
958 }
959 rows = list;
960 for (j = 0; row = rows[j]; j++) {
961 var parts = row.split(' ');
962 var cmpArray = [];
963 var part;
964 for (i = 0; part = parts[i]; i++) {
965 if (part.charAt(0) == '<') {
966 break;
967 }
968 cmpArray.push(part);
969 }
970 var cmp = cmpArray.join(' ');
971 if (token.indexOf(cmp) === 0) {
972 matches.push(row);
973 }
974 }
975 if (matches.length > maxMatches) {
976 matches.slice(0, maxMatches - 1);
977 }
978 return matches;
979 };
980
981
982 /**
983 * Compares the original and user-entered completion and returns the array
984 * of arguments if any, otherwise returns null.
985 * @param {string} str The string to parse for arguments.
986 * @param {string} originalCmd The original completion.
987 * @return {Array} Returns an array of completion arguments.
988 * @private
989 */
990 PowerKey.prototype.getArguments_ = function(str, originalCmd) {
991 str = str.replace(/\s+/g, ' ');
992 originalCmd = originalCmd.replace(/\s+/g, ' ');
993 var pos = originalCmd.indexOf('<');
994 if (pos < 0) {
995 return [];
996 }
997 originalCmd = originalCmd.substr(pos);
998 str = str.substr(pos);
999 var strTokens = str.split(',');
1000 var ostrTokens = originalCmd.split(',');
1001 if (strTokens.length != ostrTokens.length) {
1002 return [];
1003 }
1004 var args = [];
1005 for (var i = 0, j = 0, token1, token2;
1006 (token1 = strTokens[i]) && (token2 = ostrTokens[i]);
1007 i++) {
1008 token1 = PowerKey.leftTrim(PowerKey.rightTrim(token1));
1009 token2 = PowerKey.leftTrim(PowerKey.rightTrim(token2));
1010 if (token2.match(PowerKey.CMD_PARAM)) {
1011 args.push(token1);
1012 }
1013 }
1014 return args;
1015 };
1016
1017
1018 /**
1019 * The default completion handler: executes the appropriate functions
1020 * by looking at the action map.
1021 * @param {string} act The action/selection to be handled.
1022 * @param {string} originalCmd The original format of the completion without
1023 * the final parameter values.
1024 * @param {Object} actionMap The HashMap consisting of completion strings
1025 * as keys and functions as values.
1026 * @return {boolean} Whether the completion was successfully handled.
1027 * @private
1028 */
1029 PowerKey.prototype.actionHandler_ = function(act,
1030 originalCmd,
1031 actionMap) {
1032 var actionObj = actionMap[originalCmd];
1033 if (actionObj && actionObj[this.context]) {
1034 var func = actionObj[this.context];
1035 var args = this.getArguments_(act, originalCmd);
1036 if (func instanceof Function) {
1037 window.setTimeout(func(args), 0);
1038 } else {
1039 //TODO: Remove this if it is not used. Prefer the above instead.
1040 window.setTimeout(func + '(args)', 0);
1041 }
1042 return true;
1043 } else {
1044 return false;
1045 }
1046 };
1047
1048 /**
1049 * Displays the previous item in the filtered list.
1050 * @private
1051 */
1052 PowerKey.prototype.prevListItem_ = function() {
1053 if (this.listPos_ < 0) {
1054 this.listPos_ = 0;
1055 }
1056 this.listPos_ = (this.listPos_ || this.filterList_.length) - 1;
1057 if (this.listPos_ >= 0) {
1058 this.setListElement_(this.filterList_[this.listPos_]);
1059 }
1060 };
1061
1062
1063 /**
1064 * Displays the next item in the filtered list.
1065 * @private
1066 */
1067 PowerKey.prototype.nextListItem_ = function() {
1068 this.listPos_ = (this.listPos_ + 1) % this.filterList_.length;
1069 if (this.listPos_ < this.filterList_.length) {
1070 this.setListElement_(this.filterList_[this.listPos_]);
1071 }
1072 };
1073
1074
1075 /**
1076 * Displays the previous managed (named) completion list.
1077 * @private
1078 */
1079 PowerKey.prototype.prevManagedCompletionList_ = function() {
1080 var completionListsPos = this.managedCmpListsPos_ - 1;
1081 if (completionListsPos < 0) {
1082 completionListsPos = this.managedCmpLists_.length - 1;
1083 }
1084 this.setManagedCompletionList_(completionListsPos);
1085 };
1086
1087
1088 /**
1089 * Displays the next managed (named) completion list.
1090 * @private
1091 */
1092 PowerKey.prototype.nextManagedCompletionList_ = function() {
1093 var completionListsPos = this.managedCmpListsPos_ + 1;
1094 if (completionListsPos >= this.managedCmpLists_.length) {
1095 completionListsPos = 0;
1096 }
1097 this.setManagedCompletionList_(completionListsPos);
1098 };
1099
1100
1101 /**
1102 * Sets the managed (named) completion list given its position.
1103 * @param {number} managedCmpListsPos Always valid list position.
1104 * @private
1105 */
1106 PowerKey.prototype.setManagedCompletionList_ = function(managedCmpListsPos) {
1107 var managedCmpList = this.managedCmpLists_[managedCmpListsPos];
1108 this.context = managedCmpList.name;
1109 this.setCompletionList(managedCmpList.values);
1110 this.managedCmpListsPos_ = managedCmpListsPos;
1111 var status = (this.cmpFloatElement.className == 'pkVisibleStatus') ?
1112 PowerKey.status.VISIBLE : PowerKey.status.HIDDEN;
1113 this.updateCompletionField(status);
1114 if (this.managedCompletionListCallback_) {
1115 this.managedCompletionListCallback_(managedCmpList.name);
1116 }
1117 };
1118
1119
1120 /**
1121 * Selects the current completion from the list, displays it in the completion
1122 * text field and speaks it.
1123 * @private
1124 */
1125 PowerKey.prototype.selectCurrentListItem_ = function() {
1126 this.cmpTextElement.value =
1127 this.filterList_[this.listPos_ >= 0 ? this.listPos_ : 0];
1128 this.filterList_ = this.getWordFilterMatches_(this.cmpList_,
1129 this.cmpTextElement.value, 50);
1130 if (this.axsJAX_ && PowerKey.isGecko) {
1131 this.axsJAX_.speakTextViaNode(this.cmpTextElement.value);
1132 }
1133 this.listPos_ = 0;
1134 };
1135
1136
1137 /**
1138 * Speaks the element of the filtered list which is currently selected.
1139 * @param {string} text The text to be displayed as the completion field's
1140 * label (usually the selected list element itself).
1141 * @private
1142 */
1143 PowerKey.prototype.setListElement_ = function(text) {
1144 if (!this.listElement_) {
1145 this.listElement_ = document.createElement('div');
1146 this.listElement_.id = 'listElem_' + Math.floor(Math.random() * 1001);
1147 this.cmpDivElement_.appendChild(this.listElement_);
1148 }
1149 if (PowerKey.isIE) {
1150 this.listElement_.innerText = text;
1151 } else {
1152 this.listElement_.textContent = text;
1153 }
1154 if (this.browseCallback_) {
1155 this.browseCallback_(text, this.indexList_[text.toLowerCase()]);
1156 }
1157 if (this.axsJAX_ && PowerKey.isGecko) {
1158 this.axsJAX_.speakNode(this.listElement_, false);
1159 }
1160 };
1161
1162 /**
1163 * Calls the class level setDefaultCSSStyle function.
1164 * This allows PowerKey objects to be used without causing
1165 * an additional dependency if they are only used optionally.
1166 */
1167 PowerKey.prototype.setDefaultCSSStyle = function() {
1168 PowerKey.setDefaultCSSStyle();
1169 };
1170
1171 // Methods in the PowerKey class end here.
1172
1173
1174 /**
1175 * A class which creates an event handler with a pre-specified action map.
1176 * @param {Object} map A HashMap which holds key-function bindings.
1177 * @constructor
1178 */
1179 PowerKey.DefaultHandler = function(map) {
1180 /**
1181 * HashMap holding the key-function bindings.
1182 * @type {Object}
1183 */
1184 this.actionMap = map;
1185 };
1186
1187
1188 /**
1189 * The event handler to be called called inside the original event handler.
1190 * @param {Object} evt The event object passed to the event handler.
1191 * @param {PowerKey.DefaultHandler} handlerObj A reference to this.
1192 * @param {PowerKey} pkObj An object of the PowerKey class.
1193 */
1194 PowerKey.DefaultHandler.prototype.handler =
1195 function(evt, handlerObj, pkObj) {
1196 if (!handlerObj.actionMap) {
1197 return;
1198 }
1199 if (evt.keyCode) {
1200 var mapkeyCode = '' + evt.keyCode;
1201 var mapkeyChar = String.fromCharCode(evt.keyCode).toLowerCase();
1202 if (evt.ctrlKey) {
1203 mapkeyCode = 'Ctrl+' + mapkeyCode;
1204 mapkeyChar = 'Ctrl+' + mapkeyChar;
1205 }
1206 if (evt.altKey) {
1207 mapkeyCode = 'Alt+' + mapkeyCode;
1208 mapkeyChar = 'Alt+' + mapkeyChar;
1209 }
1210 if (evt.shiftKey) {
1211 mapkeyCode = 'Shift+' + mapkeyCode;
1212 mapkeyChar = 'Shift+' + mapkeyChar;
1213 }
1214 var actionObj = null;
1215 actionObj = handlerObj.actionMap[mapkeyChar];
1216 if (!actionObj) {
1217 actionObj = handlerObj.actionMap[mapkeyCode];
1218 }
1219 if (actionObj) {
1220 // If there is no action for the current context, try '*'
1221 var funcObj = actionObj[pkObj.context] ?
1222 actionObj[pkObj.context] : actionObj['*'];
1223 if (funcObj) {
1224 var func = funcObj[1];
1225 if (func) {
1226 func(evt);
1227 if (funcObj[0] != '*') {
1228 pkObj.context = funcObj[0];
1229 }
1230 }
1231 }
1232 }
1233 }
1234 };
1235
1236
1237 /**
1238 * A class to store the height and width of the viewport.
1239 * @param {number} width The width of the browser viewport.
1240 * @param {number} height The height of the browser viewport.
1241 * @constructor
1242 */
1243 PowerKey.ViewportSize = function(width, height) {
1244 this.width = width ? width : undefined;
1245 this.height = height ? height : undefined;
1246 };
1247
1248
1249 /**
1250 * Trims spaces and new lines from the left end of the string.
1251 * @param {string} str String to trim.
1252 * @return {string} The left-trimmed string.
1253 */
1254 PowerKey.leftTrim = function(str) {
1255 return str.replace(PowerKey.LEFT_TRIMMABLE, '');
1256 };
1257
1258
1259 /**
1260 * Trims spaces and new lines from the right end of the string.
1261 * @param {string} str String to trim.
1262 * @return {string} The right-trimmed string.
1263 */
1264 PowerKey.rightTrim = function(str) {
1265 return str.replace(PowerKey.RIGHT_TRIMMABLE, '');
1266 };
1267
1268
1269 /**
1270 * Escapes special characters in the string so that it can be matched against
1271 * a regular expression.
1272 * @param {string} s String from which to escape characters.
1273 * @return {string} Returns the escaped string.
1274 */
1275 PowerKey.regExpEscape = function(s) {
1276 return String(s).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
1277 replace(/\x08/g, '\\x08');
1278 };
1279
1280
1281 /**
1282 * Gets the size of the browser viewport.
1283 * @return {PowerKey.ViewportSize} Returns the size of the viewport.
1284 */
1285 PowerKey.getViewportSize = function() {
1286 var myWidth = 0, myHeight = 0;
1287 if (typeof(window.innerWidth) == 'number') {
1288 //Non-IE
1289 myWidth = window.innerWidth;
1290 myHeight = window.innerHeight;
1291 } else if (document.documentElement &&
1292 (document.documentElement.clientWidth ||
1293 document.documentElement.clientHeight)) {
1294 //IE 6+ in 'standards compliant mode'
1295 myWidth = document.documentElement.clientWidth;
1296 myHeight = document.documentElement.clientHeight;
1297 } else if (document.body &&
1298 (document.body.clientWidth ||
1299 document.body.clientHeight)) {
1300 //IE 4 compatible
1301 myWidth = document.body.clientWidth;
1302 myHeight = document.body.clientHeight;
1303 }
1304 return new PowerKey.ViewportSize(myWidth, myHeight);
1305 };
1306
1307
1308 /**
1309 * Is the user agent Internet Explorer?
1310 * @type {boolean}
1311 */
1312 PowerKey.isIE = false;
1313
1314
1315 /**
1316 * Is the user agent Firefox?
1317 * @type {boolean}
1318 */
1319 PowerKey.isGecko = false;
1320
1321
1322 /**
1323 * Detects the browser type and version.
1324 */
1325 PowerKey.setBrowser = function() {
1326 var agt = navigator.userAgent.toLowerCase();
1327 PowerKey.isGecko = (agt.indexOf('gecko') != -1);
1328 PowerKey.isIE = ((agt.indexOf('msie') != -1) &&
1329 (agt.indexOf('opera') == -1));
1330 };
1331 // Set browser type
1332 PowerKey.setBrowser();
1333
1334
1335 /**
1336 * Enumeration for events.
1337 * @enum {string}
1338 */
1339 PowerKey.Event = {
1340 KEYUP: PowerKey.isIE ? 'onkeyup' : 'keyup',
1341 KEYDOWN: PowerKey.isIE ? 'onkeydown' : 'keydown',
1342 KEYPRESS: PowerKey.isIE ? 'onkeypress' : 'keypress',
1343 CLICK: PowerKey.isIE ? 'onclick' : 'click',
1344 RESIZE: PowerKey.isIE ? 'onresize' : 'resize',
1345 FOCUS: PowerKey.isIE ? 'onfocus' : 'focus',
1346 BLUR: PowerKey.isIE ? 'onblur' : 'blur'
1347 };
1348
1349
1350 /**
1351 * CSS styles.
1352 * @type {string}
1353 */
1354 PowerKey.cssStr =
1355 '.pkHiddenStatus {display: none; position: absolute;}' +
1356 '.pkVisibleStatus {display: block; position: absolute; left: 2px; top: 2px; ' +
1357 'line-height: 1.2em; z-index: 10001; background-color: #000000; ' +
1358 'padding: 2px; color: #fff; font-family: Arial, Sans-serif; ' +
1359 'font-size: 20px; filter: alpha(opacity=80); -moz-opacity: .80;}' +
1360 '.pkOpaqueCompletionText {border-style: none; background-color:transparent; ' +
1361 'font-family: Arial, Helvetica, sans-serif; font-size: 35px; ' +
1362 'font-weight: bold; color: #fff; width: 1000px; height: 50px;}' +
1363 '.pkBackgroundShow {position: absolute; width: 0px;' +
1364 'height: 0px; background-color: #000000; filter: alpha(opacity=70); ' +
1365 ' -moz-opacity: .70; left: 0px; top: 0px; z-index: 10000;}';
1366
1367
1368 /**
1369 * Adds the PowerKey CSS to the page.
1370 */
1371 PowerKey.setDefaultCSSStyle = function() {
1372 var head, style;
1373 head = document.getElementsByTagName('head')[0];
1374 if (!head) {
1375 return;
1376 }
1377 style = document.createElement('style');
1378 style.type = 'text/css';
1379 if (PowerKey.isIE) {
1380 style.innerhtml = PowerKey.cssStr;
1381 } else if (PowerKey.isGecko) {
1382 style.innerHTML = PowerKey.cssStr;
1383 }
1384 head.appendChild(style);
1385 };
1386
1387
1388 /**
1389 * Adds the CSS tag to he DOM with href as the specified CSS file.
1390 * @param {string} cssFile The CSS file to include.
1391 */
1392 PowerKey.addCSSStyle = function(cssFile) {
1393 var headID = document.getElementsByTagName('head')[0];
1394 var cssNode = document.createElement('link');
1395 cssNode.type = 'text/css';
1396 cssNode.rel = 'stylesheet';
1397 cssNode.href = cssFile;
1398 headID.appendChild(cssNode);
1399 };
1400
1401
1402 /**
1403 * Constants for key codes.
1404 * @enum {number}
1405 */
1406 PowerKey.keyCodes = {
1407 ARROWUP: 38,
1408 ARROWDOWN: 40,
1409 ARROWLEFT: 37,
1410 ARROWRIGHT: 39,
1411 ENTER: 13,
1412 TAB: 9,
1413 ESC: 27
1414 };
1415
1416 /**
1417 * PowerKey string constants.
1418 * @enum {string}
1419 */
1420 PowerKey.str = {
1421 DEFAULT_COMPLETION_LIST_NAME: 'Completion list',
1422 DEFAULT_COMPLETION_PROMPT: 'Enter Completion',
1423 DEFAULT_NO_COMPLETION: 'No completions found'
1424 };
1425
1426 /**
1427 * Visibility status of the completion field.
1428 * @enum {string}
1429 */
1430 PowerKey.status = {
1431 VISIBLE: 'visible',
1432 HIDDEN: 'hidden'
1433 };
1434
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698