Home | History | Annotate | Download | only in elements
      1 <!--
      2   -- Copyright (c) 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" on-key-up="keyUp"
      8     on-key-down="keyDown" on-key-longpress="keyLongpress" on-pointerup="up"
      9     on-pointerdown="down" on-enable-sel="enableSel"
     10     on-enable-dbl="enableDbl" attributes="keyset layout rows">
     11   <template>
     12     <style>
     13       @host {
     14         * {
     15           position: relative;
     16         }
     17       }
     18     </style>
     19     <!-- The ID for a keyset follows the naming convention of combining the
     20       -- layout name with a base keyset name. This convention is used to
     21       -- allow multiple layouts to be loaded (enablign fast switching) while
     22       -- allowing the shift and spacebar keys to be common across multiple
     23       -- keyboard layouts.
     24       -->
     25     <content select="#{{layout}}-{{keyset}}"></content>
     26   </template>
     27   <script>
     28     /**
     29      * The repeat delay in milliseconds before a key starts repeating. Use the
     30      * same rate as Chromebook.
     31      * (See chrome/browser/chromeos/language_preferences.cc)
     32      * @const
     33      * @type {number}
     34      */
     35     var REPEAT_DELAY_MSEC = 500;
     36 
     37     /**
     38      * The repeat interval or number of milliseconds between subsequent
     39      * keypresses. Use the same rate as Chromebook.
     40      * @const
     41      * @type {number}
     42      */
     43     var REPEAT_INTERVAL_MSEC = 50;
     44 
     45     /**
     46      * The double click/tap interval.
     47      * @const
     48      * @type {number}
     49      */
     50     var DBL_INTERVAL_MSEC = 300;
     51 
     52     /**
     53      * The boolean to decide if keyboard should transit to upper case keyset
     54      * when spacebar is pressed. If a closing punctuation is followed by a
     55      * spacebar, keyboard should automatically transit to upper case.
     56      * @type {boolean}
     57      */
     58     var enterUpperOnSpace = false;
     59 
     60     /**
     61      * A structure to track the currently repeating key on the keyboard.
     62      */
     63     var repeatKey = {
     64 
     65       /**
     66         * The timer for the delay before repeating behaviour begins.
     67         * @type {number|undefined}
     68         */
     69       timer: undefined,
     70 
     71       /**
     72        * The interval timer for issuing keypresses of a repeating key.
     73        * @type {number|undefined}
     74        */
     75       interval: undefined,
     76 
     77       /**
     78        * The key which is currently repeating.
     79        * @type {BaseKey|undefined}
     80        */
     81       key: undefined,
     82 
     83       /**
     84        * Cancel the repeat timers of the currently active key.
     85        */
     86       cancel: function() {
     87         clearTimeout(this.timer);
     88         clearInterval(this.interval);
     89         this.timer = undefined;
     90         this.interval = undefined;
     91         this.key = undefined;
     92       }
     93     };
     94 
     95     /**
     96      * The minimum movement interval needed to trigger cursor move on
     97      * horizontal and vertical way.
     98      * @const
     99      * @type {number}
    100      */
    101     var MIN_SWIPE_DIST = 30;
    102 
    103     /**
    104      * The flags constants when shift is on. It is according to the EventFlags
    105      * in event_constants.h in chromium c++ code.
    106      * @const
    107      * @type {number}
    108      * TODO(zyaozhujun): Might add more flags here according to the defination
    109      * in EventFlags.
    110      */
    111     var SHIFT = 2;
    112 
    113     /**
    114      * The boolean to decide if it is swipe in process or finished.
    115      * @const
    116      * @type {boolean}
    117      */
    118     var swipeInProgress = false;
    119 
    120     /**
    121      * The enumeration of swipe directions.
    122      * @const
    123      * @type {Enum}
    124      */
    125     var SWIPE_DIRECTION = {
    126       RIGHT: 0x1,
    127       LEFT: 0x2,
    128       UP: 0x4,
    129       DOWN: 0x8
    130     };
    131 
    132     /**
    133      * A structure to track the current swipe status.
    134      */
    135     var swipeStatus = {
    136 
    137       /**
    138        * The count of horizontal and vertical movement.
    139        * @type {number}
    140        */
    141        offset_x : 0,
    142        offset_y : 0,
    143 
    144       /**
    145        * Last touch coordinate.
    146        * @type {number}
    147        */
    148       pre_x : 0,
    149       pre_y : 0,
    150 
    151       /**
    152        * The flag of current modifier key.
    153        * @type {number}
    154        */
    155       swipeFlags : 0,
    156 
    157       /**
    158        * Current swipe direction.
    159        * @type {number}
    160        */
    161       swipeDirection : 0,
    162 
    163       /**
    164        * Reset all the values when swipe finished.
    165        */
    166       resetAll: function() {
    167         this.offset_x = 0;
    168         this.offset_y = 0;
    169         this.pre_x = 0;
    170         this.pre_y = 0;
    171 	this.swipeFlags = 0;
    172 	this.swipeDirection = 0;
    173       }
    174     };
    175 
    176     Polymer('kb-keyboard', {
    177       lastPressedKey: null,
    178       voiceInput_: null,
    179       dblDetail_: null,
    180       dblTimer_: null,
    181       swipeHandler: null,
    182 
    183       ready: function() {
    184         this.voiceInput_ = new VoiceInput(this);
    185         this.swipeHandler = this.onSwipeUpdate.bind(this);
    186       },
    187 
    188       /**
    189        * When double click/tap event is enabled, the second key-down and key-up
    190        * events on the same key should be skipped. Return true when the event
    191        * with |detail| should be skipped.
    192        * @param {Object} detail The detail of key-up or key-down event.
    193        */
    194       skipEvent: function(detail) {
    195         if (this.dblDetail_) {
    196           if (this.dblDetail_.char != detail.char) {
    197             // The second key down is not on the same key. Double click/tap
    198             // should be ignored.
    199             this.dblDetail_ = null;
    200             clearTimeout(this.dblTimer_);
    201           } else if (this.dblDetail_.clickCount == 1) {
    202             return true;
    203           }
    204         }
    205         return false;
    206       },
    207 
    208       /**
    209        * This function is bound to swipeHandler. And swipeHandler handle
    210        * the pointermove event after pointerdown event happened.
    211        * @para {PointerEvent} event.
    212        */
    213       onSwipeUpdate: function(event) {
    214 	swipeStatus.offset_x += event.screenX - swipeStatus.pre_x;
    215 	swipeStatus.offset_y += event.screenY - swipeStatus.pre_y;
    216         if (Math.abs(swipeStatus.offset_x) > MIN_SWIPE_DIST ||
    217             Math.abs(swipeStatus.offset_y) > MIN_SWIPE_DIST) {
    218           swipeInProgress = true;
    219           this.lastPressedKey.classList.remove('active');
    220         }
    221         if (swipeStatus.offset_x > MIN_SWIPE_DIST) {
    222 	  swipeStatus.swipeDirection |= SWIPE_DIRECTION.RIGHT;
    223           swipeStatus.offset_x = 0;
    224         }
    225         else if (swipeStatus.offset_x < -MIN_SWIPE_DIST) {
    226 	  swipeStatus.swipeDirection |= SWIPE_DIRECTION.LEFT;
    227 	  swipeStatus.offset_x = 0;
    228 	}
    229         // Swipe vertically only when the swipe reaches the gradient of 45
    230         // degree. This can also be larger.
    231 	if (Math.abs(event.screenY - swipeStatus.pre_y) >
    232             Math.abs(event.screenX - swipeStatus.pre_x)) {
    233           if (swipeStatus.offset_y > MIN_SWIPE_DIST) {
    234             swipeStatus.swipeDirection |= SWIPE_DIRECTION.DOWN;
    235 	    swipeStatus.offset_y = 0;
    236 	  }
    237           else if (swipeStatus.offset_y < -MIN_SWIPE_DIST) {
    238 	    swipeStatus.swipeDirection |= SWIPE_DIRECTION.UP;
    239             swipeStatus.offset_y = 0;
    240 	  }
    241         }
    242        if (swipeStatus.swipeDirection) {
    243 	  MoveCursor(swipeStatus.swipeDirection, swipeStatus.swipeFlags);
    244           swipeStatus.swipeDirection = 0;
    245         }
    246 	swipeStatus.pre_x = event.screenX;
    247 	swipeStatus.pre_y = event.screenY;
    248       },
    249 
    250       /**
    251        * Handles key-down event that is sent by kb-key-base.
    252        * @param {CustomEvent} event The key-down event dispatched by
    253        *     kb-key-base.
    254        * @param {Object} detail The detail of pressed kb-key.
    255        */
    256       keyDown: function(event, detail) {
    257         if (this.skipEvent(detail))
    258           return;
    259 
    260         if (this.lastPressedKey)
    261           this.lastPressedKey.classList.remove('active');
    262         this.lastPressedKey = event.target;
    263         this.lastPressedKey.classList.add('active');
    264         repeatKey.cancel();
    265         var toKeyset = detail.toKeyset;
    266         if (toKeyset) {
    267           this.keyset = toKeyset;
    268           this.querySelector('#' + this.layout + '-' + this.keyset).nextKeyset =
    269               detail.nextKeyset;
    270           return;
    271         }
    272 
    273         if (detail.repeat) {
    274           insertText(detail.char);
    275           repeatKey.key = this.lastPressedKey;
    276           repeatKey.timer = setTimeout(function() {
    277             repeatKey.timer = undefined;
    278             repeatKey.interval = setInterval(function() {
    279                insertText(detail.char);
    280             }, REPEAT_INTERVAL_MSEC);
    281           }, Math.max(0, REPEAT_DELAY_MSEC - REPEAT_INTERVAL_MSEC));
    282         }
    283       },
    284 
    285       /**
    286        * Enable/start double click/tap event recognition.
    287        * @param {CustomEvent} event The enable-dbl event dispatched by
    288        *     kb-shift-key.
    289        * @param {Object} detail The detail of pressed kb-shift-key.
    290        */
    291       enableDbl: function(event, detail) {
    292         if (!this.dblDetail_) {
    293           this.dblDetail_ = detail;
    294           this.dblDetail_.clickCount = 0;
    295           var self = this;
    296           this.dblTimer_ = setTimeout(function() {
    297             self.dblDetail_ = null;
    298           }, DBL_INTERVAL_MSEC);
    299         }
    300       },
    301 
    302       /**
    303        * Enable the selection while swipe.
    304        * @param {CustomEvent} event The enable-dbl event dispatched by
    305        *    kb-shift-key.
    306        */
    307       enableSel: function(event) {
    308         swipeStatus.swipeFlags = SHIFT;
    309       },
    310 
    311       /**
    312        * Handles pointerdown event. This is used for swipe selection process.
    313        * to get the start pre_x and pre_y. And also add a pointermove handler
    314        * to start handling the swipe selection event.
    315        * @param {PointerEvent} event The pointerup event that received by
    316        *     kb-keyboard.
    317        */
    318       down: function(event) {
    319         swipeStatus.pre_x = event.screenX;
    320         swipeStatus.pre_y = event.screenY;
    321         this.addEventListener("pointermove", this.swipeHandler, false);
    322       },
    323 
    324       /**
    325        * Handles pointerup event. This is used for double tap/click events.
    326        * @param {PointerEvent} event The pointerup event that bubbled to
    327        *     kb-keyboard.
    328        */
    329       up: function(event) {
    330         if (this.dblDetail_) {
    331           this.dblDetail_.clickCount++;
    332           if (this.dblDetail_.clickCount == 2) {
    333             this.keyset = this.dblDetail_.toKeyset;
    334             var keysetId = '#' + this.layout + '-' + this.keyset
    335             this.querySelector(keysetId).nextKeyset = this.dblTimer_.nextKeyset;
    336             clearTimeout(this.dblTimer_);
    337             this.dblDetail_ = null;
    338           }
    339         }
    340 
    341         // TODO(zyaozhujun): There are some edge cases to deal with later.
    342         // (for instance, what if a second finger trigger a down and up
    343 	// event sequence while swiping).
    344         // When pointer up from the screen, a swipe selection session finished,
    345         // all the data should be reset to prepare for the next session.
    346         if (swipeInProgress) {
    347           swipeInProgress = false;
    348           swipeStatus.resetAll();
    349         }
    350         // Remove the pointermove event hander here.
    351         this.removeEventListener('pointermove', this.swipeHandler, false);
    352       },
    353 
    354       /**
    355        * Handles key-up event that is sent by kb-key-base.
    356        * @param {CustomEvent} event The key-up event dispatched by kb-key-base.
    357        * @param {Object} detail The detail of pressed kb-key.
    358        */
    359       keyUp: function(event, detail) {
    360         if (this.skipEvent(detail))
    361           return;
    362         if (swipeInProgress)
    363           return;
    364         this.lastPressedKey.classList.remove('active');
    365         if (this.lastPressedKey != event.target)
    366           return;
    367         if (repeatKey.key == event.target) {
    368           repeatKey.cancel();
    369           return;
    370         }
    371         var toKeyset = detail.toKeyset;
    372         // Keyset transition key.
    373         if (toKeyset) {
    374           this.keyset = toKeyset;
    375           this.querySelector('#' + this.layout + '-' + this.keyset).nextKeyset =
    376               detail.nextKeyset;
    377         }
    378         var toLayout = detail.toLayout;
    379         // Layout transition key.
    380         if (toLayout)
    381           this.layout = toLayout;
    382         var char = detail.char;
    383         if (enterUpperOnSpace) {
    384           enterUpperOnSpace = false;
    385           if (char == ' ')
    386             this.keyset = 'upper';
    387         }
    388         switch(char) {
    389           case 'Invalid':
    390           case 'Shift':
    391             swipeStatus.swipeFlags = 0;
    392             return;
    393           case 'Microphone':
    394             this.voiceInput_.onDown();
    395             return;
    396           case '.':
    397           case '?':
    398           case '!':
    399             enterUpperOnSpace = true;
    400             break;
    401           default:
    402             break;
    403         }
    404         insertText(char);
    405       },
    406 
    407       /*
    408        * Handles key-longpress event that is sent by kb-key-base.
    409        * @param {CustomEvent} event The key-longpress event dispatched by
    410        *     kb-key-base.
    411        * @param {Object} detail The detail of pressed key.
    412        */
    413       keyLongpress: function(event, detail) {
    414         var toKeyset = detail.toKeyset;
    415         // Keyset transtion key.
    416         if (toKeyset) {
    417           this.keyset = toKeyset;
    418           this.querySelector('#' + this.layout + '-' + this.keyset).nextKeyset =
    419               detail.nextKeyset;
    420           // Makes last pressed key inactive if transit to a new keyset on long
    421           // press.
    422           this.lastPressedKey.classList.remove('active');
    423         }
    424       },
    425 
    426       /**
    427        * Handles a change in the keyboard layout.  Auto-selects the default
    428        * keyset for the new layout.
    429        */
    430       layoutChanged: function() {
    431         if (!this.selectDefaultKeyset()) {
    432           // Keyset selection fails if the keysets have not been loaded yet.
    433           var keysets = document.querySelector('#' + this.layout);
    434           if (keysets) {
    435             keyboard.appendChild(flattenKeysets(keysets.content));
    436             this.selectDefaultKeyset();
    437           } else {
    438             console.error('Unable to find layout ' + this.layout);
    439          }
    440         }
    441       },
    442 
    443       /**
    444        * Selects the default keyset for a layout.
    445        * @return {boolean} True if successful.  This method can fail if the
    446        *     keysets corresponding to the layout have not been injected.
    447        */
    448       selectDefaultKeyset: function() {
    449         var keysets = this.querySelectorAll('kb-keyset');
    450         // Full name of the keyset is of the form 'layout-keyset'.
    451         var regex = new RegExp('^' + this.layout + '-(.+)');
    452         var keysetsLoaded = false;
    453         for (var i = 0; i < keysets.length; i++) {
    454           var matches = keysets[i].id.match(regex);
    455           if (matches && matches.length == 2) {
    456              keysetsLoaded = true;
    457              if (keysets[i].isDefault) {
    458                this.keyset = matches[1];
    459                return true;
    460              }
    461           }
    462         }
    463         if (keysetsLoaded)
    464           console.error('No default keyset found for ' + this.layout);
    465         return false;
    466       }
    467     });
    468   </script>
    469 </polymer-element>
    470 
    471