Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / ui / keyboard / resources / elements / kb-keyboard.html
1 <!--
2   -- Copyright 2013 The Chromium Authors. All rights reserved.
3   -- Use of this source code is governed by a BSD-style license that can be
4   -- found in the LICENSE file.
5   -->
6
7 <polymer-element name="kb-keyboard" on-key-over="{{keyOver}}"
8     on-key-up="{{keyUp}}" on-key-down="{{keyDown}}"
9     on-key-longpress="{{keyLongpress}}" on-pointerup="{{up}}"
10     on-pointerdown="{{down}}" on-pointerout="{{out}}"
11     on-enable-sel="{{enableSel}}" on-enable-dbl="{{enableDbl}}"
12     on-key-out="{{keyOut}}" on-show-options="{{showOptions}}"
13     on-set-layout="{{setLayout}}" on-type-key="{{type}}"
14     attributes="keyset layout inputType inputTypeToLayoutMap">
15   <template>
16     <style>
17       :host {
18         bottom: 0;
19         left: 0;
20         position: absolute;
21         right: 0;
22         top: 0;
23       }
24     </style>
25     <!-- The ID for a keyset follows the naming convention of combining the
26       -- layout name with a base keyset name. This convention is used to
27       -- allow multiple layouts to be loaded (enablign fast switching) while
28       -- allowing the shift and spacebar keys to be common across multiple
29       -- keyboard layouts.
30       -->
31     <content id="content"></content>
32     <kb-keyboard-overlay id="overlay" hidden></kb-keyboard-overlay>
33     <kb-key-codes id="keyCodeMetadata"></kb-key-codes>
34
35   </template>
36   <script>
37     /**
38      * The repeat delay in milliseconds before a key starts repeating. Use the
39      * same rate as Chromebook.
40      * (See chrome/browser/chromeos/language_preferences.cc)
41      * @const
42      * @type {number}
43      */
44     var REPEAT_DELAY_MSEC = 500;
45
46     /**
47      * The repeat interval or number of milliseconds between subsequent
48      * keypresses. Use the same rate as Chromebook.
49      * @const
50      * @type {number}
51      */
52     var REPEAT_INTERVAL_MSEC = 50;
53
54     /**
55      * The double click/tap interval.
56      * @const
57      * @type {number}
58      */
59     var DBL_INTERVAL_MSEC = 300;
60
61     /**
62      * The index of the name of the keyset when searching for all keysets.
63      * @const
64      * @type {number}
65      */
66     var REGEX_KEYSET_INDEX = 1;
67
68     /**
69      * The integer number of matches when searching for keysets.
70      * @const
71      * @type {number}
72      */
73     var REGEX_MATCH_COUNT = 2;
74
75     /**
76      * The boolean to decide if keyboard should transit to upper case keyset
77      * when spacebar is pressed. If a closing punctuation is followed by a
78      * spacebar, keyboard should automatically transit to upper case.
79      * @type {boolean}
80      */
81     var enterUpperOnSpace = false;
82
83     /**
84      * A structure to track the currently repeating key on the keyboard.
85      */
86     var repeatKey = {
87
88       /**
89         * The timer for the delay before repeating behaviour begins.
90         * @type {number|undefined}
91         */
92       timer: undefined,
93
94       /**
95        * The interval timer for issuing keypresses of a repeating key.
96        * @type {number|undefined}
97        */
98       interval: undefined,
99
100       /**
101        * The key which is currently repeating.
102        * @type {BaseKey|undefined}
103        */
104       key: undefined,
105
106       /**
107        * Cancel the repeat timers of the currently active key.
108        */
109       cancel: function() {
110         clearTimeout(this.timer);
111         clearInterval(this.interval);
112         this.timer = undefined;
113         this.interval = undefined;
114         this.key = undefined;
115       }
116     };
117
118     /**
119      * The minimum movement interval needed to trigger cursor move on
120      * horizontal and vertical way.
121      * @const
122      * @type {number}
123      */
124     var MIN_SWIPE_DIST_X = 50;
125     var MIN_SWIPE_DIST_Y = 20;
126
127     /**
128      * The maximum swipe distance that will trigger hintText of a key
129      * to be typed.
130      * @const
131      * @type {number}
132      */
133     var MAX_SWIPE_FLICK_DIST = 60;
134
135     /**
136      * The boolean to decide if it is swipe in process or finished.
137      * @type {boolean}
138      */
139     var swipeInProgress = false;
140
141     // Flag values for ctrl, alt and shift as defined by EventFlags
142     // in "event_constants.h".
143     // @enum {number}
144     var Modifier = {
145       NONE: 0,
146       ALT: 8,
147       CONTROL: 4,
148       SHIFT: 2
149     };
150
151     /**
152      * A structure to track the current swipe status.
153      */
154     var swipeTracker = {
155       /**
156        * The latest PointerMove event in the swipe.
157        * @type {Object}
158        */
159       currentEvent: undefined,
160
161       /**
162        * Whether or not a swipe changes direction.
163        * @type {false}
164        */
165       isComplex: false,
166
167       /**
168        * The count of horizontal and vertical movement.
169        * @type {number}
170        */
171       offset_x : 0,
172       offset_y : 0,
173
174       /**
175        * Last touch coordinate.
176        * @type {number}
177        */
178       pre_x : 0,
179       pre_y : 0,
180
181       /**
182        * The PointerMove event which triggered the swipe.
183        * @type {Object}
184        */
185       startEvent: undefined,
186
187       /**
188        * The flag of current modifier key.
189        * @type {number}
190        */
191       swipeFlags : 0,
192
193       /**
194        * Current swipe direction.
195        * @type {number}
196        */
197       swipeDirection : 0,
198
199       /**
200        * The number of times we've swiped within a single swipe.
201        * @type {number}
202        */
203       swipeIndex: 0,
204
205       /**
206        * Returns the combined direction of the x and y offsets.
207        * @return {number} The latest direction.
208        */
209       getOffsetDirection: function() {
210         // TODO (rsadam): Use angles to figure out the direction.
211         var direction = 0;
212         // Checks for horizontal swipe.
213         if (Math.abs(this.offset_x) > MIN_SWIPE_DIST_X) {
214           if (this.offset_x > 0) {
215             direction |= SwipeDirection.RIGHT;
216           } else {
217             direction |= SwipeDirection.LEFT;
218           }
219         }
220         // Checks for vertical swipe.
221         if (Math.abs(this.offset_y) > MIN_SWIPE_DIST_Y) {
222           if (this.offset_y < 0) {
223             direction |= SwipeDirection.UP;
224           } else {
225             direction |= SwipeDirection.DOWN;
226           }
227         }
228         return direction;
229       },
230
231       /**
232        * Populates the swipe update details.
233        * @param {boolean} endSwipe Whether this is the final event for this
234        *     swipe.
235        * @return {Object} The current state of the swipeTracker.
236        */
237       populateDetails: function(endSwipe) {
238         var detail = {};
239         detail.direction = this.swipeDirection;
240         detail.index = this.swipeIndex;
241         detail.status = this.swipeStatus;
242         detail.endSwipe = endSwipe;
243         detail.startEvent = this.startEvent;
244         detail.currentEvent = this.currentEvent;
245         detail.isComplex = this.isComplex;
246         return detail;
247       },
248
249       /**
250        * Reset all the values when swipe finished.
251        */
252       resetAll: function() {
253         this.offset_x = 0;
254         this.offset_y = 0;
255         this.pre_x = 0;
256         this.pre_y = 0;
257         this.swipeFlags = 0;
258         this.swipeDirection = 0;
259         this.swipeIndex = 0;
260         this.startEvent = undefined;
261         this.currentEvent = undefined;
262         this.isComplex = false;
263       },
264
265       /**
266        * Updates the swipe path with the current event.
267        * @param {Object} event The PointerEvent that triggered this update.
268        * @return {boolean} Whether or not to notify swipe observers.
269        */
270       update: function(event) {
271         if(!event.isPrimary)
272           return false;
273         // Update priors.
274         this.offset_x += event.screenX - this.pre_x;
275         this.offset_y += event.screenY - this.pre_y;
276         this.pre_x = event.screenX;
277         this.pre_y = event.screenY;
278
279         // Check if movement crosses minimum thresholds in each direction.
280         var direction = this.getOffsetDirection();
281         if (direction == 0)
282           return false;
283         // If swipeIndex is zero the current event is triggering the swipe.
284         if (this.swipeIndex == 0) {
285           this.startEvent = event;
286         } else if (direction != this.swipeDirection) {
287           // Toggle the isComplex flag.
288           this.isComplex = true;
289         }
290         // Update the swipe tracker.
291         this.swipeDirection = direction;
292         this.offset_x = 0;
293         this.offset_y = 0;
294         this.currentEvent = event;
295         this.swipeIndex++;
296         return true;
297       },
298
299     };
300
301     Polymer('kb-keyboard', {
302       alt: null,
303       config: null,
304       control: null,
305       dblDetail_: null,
306       dblTimer_: null,
307       inputType: null,
308       lastPressedKey: null,
309       shift: null,
310       stale: true,
311       swipeHandler: null,
312       voiceInput_: null,
313
314       /**
315        * The default input type to keyboard layout map. The key must be one of
316        * the input box type values.
317        * @type {object}
318        */
319       inputTypeToLayoutMap: {
320         number: "numeric",
321         text: "qwerty",
322         password: "qwerty"
323       },
324
325       /**
326        * Changes the current keyset.
327        * @param {Object} detail The detail of the event that called this
328        *     function.
329        */
330       changeKeyset: function(detail) {
331         if (detail.relegateToShift && this.shift) {
332           this.keyset = this.shift.textKeyset;
333           this.activeKeyset.nextKeyset = undefined;
334           return true;
335         }
336         var toKeyset = detail.toKeyset;
337         if (toKeyset) {
338           this.keyset = toKeyset;
339           this.activeKeyset.nextKeyset = detail.nextKeyset;
340           return true;
341         }
342         return false;
343       },
344
345       keysetChanged: function() {
346         var keyset = this.activeKeyset;
347         // Show the keyset if it has been initialized.
348         if (keyset)
349           keyset.show();
350       },
351
352       configChanged: function() {
353         this.layout = this.config.layout;
354       },
355
356       ready: function() {
357         this.voiceInput_ = new VoiceInput(this);
358         this.swipeHandler = this.move.bind(this);
359         var self = this;
360         getKeyboardConfig(function(config) {
361           self.config = config;
362         });
363       },
364
365       /**
366        * Registers a callback for state change events.
367        * @param{!Function} callback Callback function to register.
368        */
369       addKeysetChangedObserver: function(callback) {
370         this.addEventListener('stateChange', callback);
371       },
372
373       /**
374        * Called when the type of focused input box changes. If a keyboard layout
375        * is defined for the current input type, that layout will be loaded.
376        * Otherwise, the keyboard layout for 'text' type will be loaded.
377        */
378       inputTypeChanged: function() {
379         // Disable layout switching at accessbility mode.
380         if (this.config && this.config.a11ymode)
381           return;
382
383         // TODO(bshe): Toggle visibility of some keys in a keyboard layout
384         // according to the input type.
385         var layout = this.inputTypeToLayoutMap[this.inputType];
386         if (!layout)
387           layout = this.inputTypeToLayoutMap.text;
388         this.layout = layout;
389       },
390
391       /**
392        * When double click/tap event is enabled, the second key-down and key-up
393        * events on the same key should be skipped. Return true when the event
394        * with |detail| should be skipped.
395        * @param {Object} detail The detail of key-up or key-down event.
396        */
397       skipEvent: function(detail) {
398         if (this.dblDetail_) {
399           if (this.dblDetail_.char != detail.char) {
400             // The second key down is not on the same key. Double click/tap
401             // should be ignored.
402             this.dblDetail_ = null;
403             clearTimeout(this.dblTimer_);
404           } else if (this.dblDetail_.clickCount == 1) {
405             return true;
406           }
407         }
408         return false;
409       },
410
411       /**
412        * Handles a swipe update.
413        * param {Object} detail The swipe update details.
414        */
415       onSwipeUpdate: function(detail) {
416         var direction = detail.direction;
417         if (!direction)
418           console.error("Swipe direction cannot be: " + direction);
419         // Triggers swipe editting if it's a purely horizontal swipe.
420         if (!(direction & (SwipeDirection.UP | SwipeDirection.DOWN))) {
421           // Nothing to do if the swipe has ended.
422           if (detail.endSwipe)
423             return;
424           var modifiers = 0;
425           // TODO (rsadam): This doesn't take into account index shifts caused
426           // by vertical swipes.
427           if (detail.index % 2 != 0) {
428             modifiers |= Modifier.SHIFT;
429             modifiers |= Modifier.CONTROL;
430           }
431           MoveCursor(direction, modifiers);
432           return;
433         }
434         // Triggers swipe hintText if it's a purely vertical swipe.
435         if (!(direction & (SwipeDirection.LEFT | SwipeDirection.RIGHT))) {
436           // Check if event is relevant to us.
437           if ((!detail.endSwipe) || (detail.isComplex))
438             return;
439           // Too long a swipe.
440           var distance = Math.abs(detail.startEvent.screenY -
441               detail.currentEvent.screenY);
442           if (distance > MAX_SWIPE_FLICK_DIST)
443             return;
444           var triggerKey = detail.startEvent.target;
445           if (triggerKey && triggerKey.onFlick)
446             triggerKey.onFlick(detail);
447         }
448       },
449
450       /**
451        * This function is bound to swipeHandler. Updates the current swipe
452        * status so that PointerEvents can be converted to Swipe events.
453        * @param {PointerEvent} event.
454        */
455       move: function(event) {
456         if (!swipeTracker.update(event))
457           return;
458         // Conversion was successful, swipe is now in progress.
459         swipeInProgress = true;
460         if (this.lastPressedKey) {
461           this.lastPressedKey.classList.remove('active');
462           this.lastPressedKey = null;
463         }
464         this.onSwipeUpdate(swipeTracker.populateDetails(false));
465       },
466
467       /**
468        * Handles key-down event that is sent by kb-key-base.
469        * @param {CustomEvent} event The key-down event dispatched by
470        *     kb-key-base.
471        * @param {Object} detail The detail of pressed kb-key.
472        */
473       keyDown: function(event, detail) {
474         if (this.skipEvent(detail))
475           return;
476
477         if (this.lastPressedKey) {
478           this.lastPressedKey.classList.remove('active');
479           this.lastPressedKey.autoRelease();
480         }
481         this.lastPressedKey = event.target;
482         this.lastPressedKey.classList.add('active');
483         repeatKey.cancel();
484
485         var char = detail.char;
486         switch(char) {
487           case 'Shift':
488             this.classList.remove('caps-locked');
489             break;
490           case 'Alt':
491           case 'Ctrl':
492             var modifier = char.toLowerCase() + "-active";
493             // Removes modifier if already active.
494             if (this.classList.contains(modifier))
495               this.classList.remove(modifier);
496             break;
497           case 'Invalid':
498             // Not all Invalid keys are transition keys. Reset control keys if
499             // we pressed a transition key.
500             if (event.target.toKeyset || detail.relegateToShift)
501               this.onNonControlKeyTyped();
502             break;
503           default:
504             // Notify shift key.
505             if (this.shift)
506               this.shift.onNonControlKeyDown();
507             if (this.ctrl)
508               this.ctrl.onNonControlKeyDown();
509             if (this.alt)
510               this.alt.onNonControlKeyDown();
511             break;
512         }
513         if(this.changeKeyset(detail))
514           return;
515         if (detail.repeat) {
516           this.keyTyped(detail);
517           this.onNonControlKeyTyped();
518           repeatKey.key = this.lastPressedKey;
519           var self = this;
520           repeatKey.timer = setTimeout(function() {
521             repeatKey.timer = undefined;
522             repeatKey.interval = setInterval(function() {
523                self.keyTyped(detail);
524             }, REPEAT_INTERVAL_MSEC);
525           }, Math.max(0, REPEAT_DELAY_MSEC - REPEAT_INTERVAL_MSEC));
526         }
527       },
528
529       /**
530        * Handles key-out event that is sent by kb-shift-key.
531        * @param {CustomEvent} event The key-out event dispatched by
532        *     kb-shift-key.
533        * @param {Object} detail The detail of pressed kb-shift-key.
534        */
535       keyOut: function(event, detail) {
536         this.changeKeyset(detail);
537       },
538
539       /**
540        * Enable/start double click/tap event recognition.
541        * @param {CustomEvent} event The enable-dbl event dispatched by
542        *     kb-shift-key.
543        * @param {Object} detail The detail of pressed kb-shift-key.
544        */
545       enableDbl: function(event, detail) {
546         if (!this.dblDetail_) {
547           this.dblDetail_ = detail;
548           this.dblDetail_.clickCount = 0;
549           var self = this;
550           this.dblTimer_ = setTimeout(function() {
551             self.dblDetail_.callback = null;
552             self.dblDetail_ = null;
553           }, DBL_INTERVAL_MSEC);
554         }
555       },
556
557       /**
558        * Enable the selection while swipe.
559        * @param {CustomEvent} event The enable-dbl event dispatched by
560        *    kb-shift-key.
561        */
562       enableSel: function(event) {
563         // TODO(rsadam): Disabled for now. May come back if we revert swipe
564         // selection to not do word selection.
565       },
566
567       /**
568        * Handles pointerdown event. This is used for swipe selection process.
569        * to get the start pre_x and pre_y. And also add a pointermove handler
570        * to start handling the swipe selection event.
571        * @param {PointerEvent} event The pointerup event that received by
572        *     kb-keyboard.
573        */
574       down: function(event) {
575         if (event.isPrimary) {
576           swipeTracker.pre_x = event.screenX;
577           swipeTracker.pre_y = event.screenY;
578           this.addEventListener("pointermove", this.swipeHandler, false);
579         }
580       },
581
582       /**
583        * Handles pointerup event. This is used for double tap/click events.
584        * @param {PointerEvent} event The pointerup event that bubbled to
585        *     kb-keyboard.
586        */
587       up: function(event) {
588         // When touch typing, it is very possible that finger moves slightly out
589         // of the key area before releases. The key should not be dropped in
590         // this case.
591         if (this.lastPressedKey &&
592             this.lastPressedKey.pointerId == event.pointerId) {
593           this.lastPressedKey.autoRelease();
594         }
595
596         if (this.dblDetail_) {
597           this.dblDetail_.clickCount++;
598           if (this.dblDetail_.clickCount == 2) {
599             this.dblDetail_.callback();
600             this.changeKeyset(this.dblDetail_);
601             clearTimeout(this.dblTimer_);
602
603             this.classList.add('caps-locked');
604
605             this.dblDetail_ = null;
606           }
607         }
608
609         // TODO(zyaozhujun): There are some edge cases to deal with later.
610         // (for instance, what if a second finger trigger a down and up
611         // event sequence while swiping).
612         // When pointer up from the screen, a swipe selection session finished,
613         // all the data should be reset to prepare for the next session.
614         if (event.isPrimary && swipeInProgress) {
615           swipeInProgress = false;
616           this.onSwipeUpdate(swipeTracker.populateDetails(true))
617           swipeTracker.resetAll();
618         }
619         this.removeEventListener('pointermove', this.swipeHandler, false);
620       },
621
622       /**
623        * Handles PointerOut event. This is used for when a swipe gesture goes
624        * outside of the keyboard window.
625        * @param {Object} event The pointerout event that bubbled to the
626        *    kb-keyboard.
627        */
628       out: function(event) {
629         // Ignore if triggered from one of the keys.
630         if (this.compareDocumentPosition(event.relatedTarget) &
631             Node.DOCUMENT_POSITION_CONTAINED_BY)
632           return;
633         if (swipeInProgress)
634           this.onSwipeUpdate(swipeTracker.populateDetails(true))
635         // Touched outside of the keyboard area, so disables swipe.
636         swipeInProgress = false;
637         swipeTracker.resetAll();
638         this.removeEventListener('pointermove', this.swipeHandler, false);
639       },
640
641       /**
642        * Handles a TypeKey event. This is used for when we programmatically
643        * want to type a specific key.
644        * @param {CustomEvent} event The TypeKey event that bubbled to the
645        *    kb-keyboard.
646        */
647       type: function(event) {
648         this.keyTyped(event.detail);
649       },
650
651       /**
652        * Handles key-up event that is sent by kb-key-base.
653        * @param {CustomEvent} event The key-up event dispatched by kb-key-base.
654        * @param {Object} detail The detail of pressed kb-key.
655        */
656       keyUp: function(event, detail) {
657         if (this.skipEvent(detail))
658           return;
659         if (swipeInProgress)
660           return;
661         if (detail.activeModifier) {
662           var modifier = detail.activeModifier.toLowerCase() + "-active";
663           this.classList.add(modifier);
664         }
665         // Adds the current keyboard modifiers to the detail.
666         if (this.ctrl)
667           detail.controlModifier = this.ctrl.isActive();
668         if (this.alt)
669           detail.altModifier = this.alt.isActive();
670         if (this.lastPressedKey)
671           this.lastPressedKey.classList.remove('active');
672         // Keyset transition key. This is needed to transition from upper
673         // to lower case when we are not in caps mode, as well as when
674         // we're ending chording.
675         this.changeKeyset(detail);
676
677         if (this.lastPressedKey &&
678             this.lastPressedKey.charValue != event.target.charValue) {
679           return;
680         }
681         if (repeatKey.key == event.target) {
682           repeatKey.cancel();
683           this.lastPressedKey = null;
684           return;
685         }
686         var toLayoutId = detail.toLayout;
687         // Layout transition key.
688         if (toLayoutId)
689           this.layout = toLayoutId;
690         var char = detail.char;
691         this.lastPressedKey = null;
692         // Characters that should not be typed.
693         switch(char) {
694           case 'Invalid':
695           case 'Shift':
696           case 'Ctrl':
697           case 'Alt':
698             enterUpperOnSpace = false;
699             swipeTracker.swipeFlags = 0;
700             return;
701           case 'Microphone':
702             this.voiceInput_.onDown();
703             return;
704           default:
705             break;
706         }
707         // Tries to type the character. Resorts to insertText if that fails.
708         if(!this.keyTyped(detail))
709           insertText(char);
710         // Post-typing logic.
711         switch(char) {
712           case ' ':
713             if(enterUpperOnSpace) {
714               enterUpperOnSpace = false;
715               if (this.shift) {
716                 var shiftDetail = this.shift.onSpaceAfterPunctuation();
717                 // Check if transition defined.
718                 this.changeKeyset(shiftDetail);
719               } else {
720                 console.error('Capitalization on space after punctuation \
721                             enabled, but cannot find target keyset.');
722               }
723               // Immediately return to maintain shift-state. Space is a
724               // non-control key and would otherwise trigger a reset of the
725               // shift key, causing a transition to lower case.
726               // TODO(rsadam): Add unit test after Polymer uprev complete.
727               return;
728             }
729             break;
730           case '.':
731           case '?':
732           case '!':
733             enterUpperOnSpace = this.shouldUpperOnSpace();
734             break;
735           default:
736             break;
737         }
738         // Reset control keys.
739         this.onNonControlKeyTyped();
740       },
741
742       /*
743        * Handles key-longpress event that is sent by kb-key-base.
744        * @param {CustomEvent} event The key-longpress event dispatched by
745        *     kb-key-base.
746        * @param {Object} detail The detail of pressed key.
747        */
748       keyLongpress: function(event, detail) {
749         // If the gesture is long press, remove the pointermove listener.
750         this.removeEventListener('pointermove', this.swipeHandler, false);
751         // Keyset transtion key.
752         if (this.changeKeyset(detail)) {
753           // Locks the keyset before removing active to prevent flicker.
754           this.classList.add('caps-locked');
755           // Makes last pressed key inactive if transit to a new keyset on long
756           // press.
757           if (this.lastPressedKey)
758             this.lastPressedKey.classList.remove('active');
759         }
760       },
761
762       /**
763        * Whether we should transit to upper case when seeing a space after
764        * punctuation.
765        * @return {boolean}
766        */
767       shouldUpperOnSpace: function() {
768         // TODO(rsadam): Add other input types in which we should not
769         // transition to upper after a space.
770         return this.inputTypeValue != 'password';
771       },
772
773       /**
774        * Show menu for selecting a keyboard layout.
775        * @param {!Event} event The triggering event.
776        * @param {{left: number, top: number, width: number}} details Location of
777        *     the button that triggered the popup.
778        */
779       showOptions: function(event, details) {
780         var overlay = this.$.overlay;
781         if (!overlay) {
782           console.error('Missing overlay.');
783           return;
784         }
785         var menu = overlay.$.options;
786         if (!menu) {
787            console.error('Missing options menu.');
788         }
789         menu.hidden = false;
790         overlay.hidden = false;
791         var left = details.left + details.width - menu.clientWidth;
792         var top = details.top - menu.clientHeight;
793         menu.style.left = left + 'px';
794         menu.style.top = top + 'px';
795       },
796
797       /**
798        * Handler for the 'set-layout' event.
799        * @param {!Event} event The triggering event.
800        * @param {{layout: string}} details Details of the event, which contains
801        *     the name of the layout to activate.
802        */
803       setLayout: function(event, details) {
804         this.layout = details.layout;
805       },
806
807       /**
808        * Handles a change in the keyboard layout. Auto-selects the default
809        * keyset for the new layout.
810        */
811       layoutChanged: function() {
812         this.stale = true;
813         if (!this.selectDefaultKeyset()) {
814           this.fire('stateChange', {state: 'loadingKeyset'});
815
816           // Keyset selection fails if the keysets have not been loaded yet.
817           var keysets = document.querySelector('#' + this.layout);
818           if (keysets && keysets.content) {
819             var content = flattenKeysets(keysets.content);
820             this.appendChild(content);
821             this.selectDefaultKeyset();
822           } else {
823             // Add link for the keysets if missing from the document. Force
824             // a layout change after resolving the import of the link.
825             var query = 'link[id=' + this.layout + ']';
826             if (!document.querySelector(query)) {
827               // Layout has not beeen loaded yet.
828               var link = document.createElement('link');
829               link.id = this.layout;
830               link.setAttribute('rel', 'import');
831               link.setAttribute('href', 'layouts/' + this.layout + '.html');
832               document.head.appendChild(link);
833
834               // Load content for the new link element.
835               var self = this;
836               HTMLImports.importer.load(document, function() {
837                 HTMLImports.parser.parseLink(link);
838                 self.layoutChanged();
839               });
840             }
841           }
842         // New keyset has already been loaded, can show immediately.
843         } else {
844           this.activeKeyset.show();
845         }
846       },
847
848       /**
849        * Notifies the modifier keys that a non-control key was typed. This
850        * lets them reset sticky behaviour. A non-control key is defined as
851        * any key that is not Control, Alt, or Shift.
852        */
853       onNonControlKeyTyped: function() {
854         if (this.shift)
855           this.shift.onNonControlKeyTyped();
856         if (this.ctrl)
857           this.ctrl.onNonControlKeyTyped();
858         if (this.alt)
859           this.alt.onNonControlKeyTyped();
860         this.classList.remove('ctrl-active');
861         this.classList.remove('alt-active');
862       },
863
864       /**
865        * Id for the active keyset.
866        * @type {string}
867        */
868       get activeKeysetId() {
869         return this.layout + '-' + this.keyset;
870       },
871
872       /**
873        * The active keyset DOM object.
874        * @type {kb-keyset}
875        */
876       get activeKeyset() {
877         return this.querySelector('#' + this.activeKeysetId);
878       },
879
880       /**
881        * The current input type.
882        * @type {string}
883        */
884       get inputTypeValue() {
885         return this.inputType;
886       },
887
888       /**
889        * Changes the input type if it's different from the current
890        * type, else resets the keyset to the default keyset.
891        * @type {string}
892        */
893       set inputTypeValue(value) {
894         if (value == this.inputType)
895           this.selectDefaultKeyset();
896         else
897           this.inputType = value;
898       },
899
900       /**
901        * The keyboard is ready for input once the target keyset appears
902        * in the distributed nodes for the keyboard.
903        * @return {boolean} Indicates if the keyboard is ready for input.
904        */
905       isReady: function() {
906         var keyset =  this.activeKeyset;
907         if (!keyset)
908           return false;
909         var nodes = this.$.content.getDistributedNodes();
910         for (var i = 0; i < nodes.length; i++) {
911           if (nodes[i].id && nodes[i].id == keyset.id)
912             return true;
913         }
914         return false;
915       },
916
917       /**
918        * Generates fabricated key events to simulate typing on a
919        * physical keyboard.
920        * @param {Object} detail Attributes of the key being typed.
921        * @return {boolean} Whether the key type succeeded.
922        */
923       keyTyped: function(detail) {
924         var builder = this.$.keyCodeMetadata;
925         if (this.shift)
926           detail.shiftModifier = this.shift.isActive();
927         if (this.ctrl)
928           detail.controlModifier = this.ctrl.isActive();
929         if (this.alt)
930           detail.altModifier = this.alt.isActive();
931         var downEvent = builder.createVirtualKeyEvent(detail, "keydown");
932         if (downEvent) {
933           sendKeyEvent(downEvent);
934           sendKeyEvent(builder.createVirtualKeyEvent(detail, "keyup"));
935           return true;
936         }
937         return false;
938       },
939
940       /**
941        * Selects the default keyset for a layout.
942        * @return {boolean} True if successful. This method can fail if the
943        *     keysets corresponding to the layout have not been injected.
944        */
945       selectDefaultKeyset: function() {
946         var keysets = this.querySelectorAll('kb-keyset');
947         // Full name of the keyset is of the form 'layout-keyset'.
948         var regex = new RegExp('^' + this.layout + '-(.+)');
949         var keysetsLoaded = false;
950         for (var i = 0; i < keysets.length; i++) {
951           var matches = keysets[i].id.match(regex);
952           if (matches && matches.length == REGEX_MATCH_COUNT) {
953              keysetsLoaded = true;
954              // Without both tests for a default keyset, it is possible to get
955              // into a state where multiple layouts are displayed.  A
956              // reproducable test case is do the following set of keyset
957              // transitions: qwerty -> system -> dvorak -> qwerty.
958              // TODO(kevers): Investigate why this is the case.
959              if (keysets[i].isDefault ||
960                  keysets[i].getAttribute('isDefault') == 'true') {
961                this.keyset = matches[REGEX_KEYSET_INDEX];
962                this.classList.remove('caps-locked');
963                this.classList.remove('alt-active');
964                this.classList.remove('ctrl-active');
965                // Caches shift key.
966                this.shift = this.querySelector('kb-shift-key');
967                if (this.shift)
968                  this.shift.reset();
969                // Caches control key.
970                this.ctrl = this.querySelector('kb-modifier-key[char=Ctrl]');
971                if (this.ctrl)
972                  this.ctrl.reset();
973                // Caches alt key.
974                this.alt = this.querySelector('kb-modifier-key[char=Alt]');
975                if (this.alt)
976                  this.alt.reset();
977                this.fire('stateChange', {
978                  state: 'keysetLoaded',
979                  value: this.keyset,
980                });
981                keyboardLoaded();
982                return true;
983              }
984           }
985         }
986         if (keysetsLoaded)
987           console.error('No default keyset found for ' + this.layout);
988         return false;
989       }
990     });
991   </script>
992 </polymer-element>