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