Home | History | Annotate | Download | only in elements
      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   
    979 
    980