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.
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" on-key-out="keyOut" on-show-options="showOptions"
11 on-set-layout="setLayout"
12 attributes="keyset layout inputType inputTypeToLayoutMap">
21 <!-- The ID for a keyset follows the naming convention of combining the
22 -- layout name with a base keyset name. This convention is used to
23 -- allow multiple layouts to be loaded (enablign fast switching) while
24 -- allowing the shift and spacebar keys to be common across multiple
27 <content select="#{{layout}}-{{keyset}}"></content>
28 <kb-keyboard-overlay id="overlay" hidden></kb-keyboard-overlay>
29 <kb-key-codes id="keyCodeMetadata"></kb-key-codes>
33 * The repeat delay in milliseconds before a key starts repeating. Use the
34 * same rate as Chromebook.
35 * (See chrome/browser/chromeos/language_preferences.cc)
39 var REPEAT_DELAY_MSEC = 500;
42 * The repeat interval or number of milliseconds between subsequent
43 * keypresses. Use the same rate as Chromebook.
47 var REPEAT_INTERVAL_MSEC = 50;
50 * The double click/tap interval.
54 var DBL_INTERVAL_MSEC = 300;
57 * The index of the name of the keyset when searching for all keysets.
61 var REGEX_KEYSET_INDEX = 1;
64 * The integer number of matches when searching for keysets.
68 var REGEX_MATCH_COUNT = 2;
71 * The boolean to decide if keyboard should transit to upper case keyset
72 * when spacebar is pressed. If a closing punctuation is followed by a
73 * spacebar, keyboard should automatically transit to upper case.
76 var enterUpperOnSpace = false;
79 * A structure to track the currently repeating key on the keyboard.
84 * The timer for the delay before repeating behaviour begins.
85 * @type {number|undefined}
90 * The interval timer for issuing keypresses of a repeating key.
91 * @type {number|undefined}
96 * The key which is currently repeating.
97 * @type {BaseKey|undefined}
102 * Cancel the repeat timers of the currently active key.
105 clearTimeout(this.timer);
106 clearInterval(this.interval);
107 this.timer = undefined;
108 this.interval = undefined;
109 this.key = undefined;
114 * The minimum movement interval needed to trigger cursor move on
115 * horizontal and vertical way.
119 var MIN_SWIPE_DIST = 60;
122 * The boolean to decide if it is swipe in process or finished.
125 var swipeInProgress = false;
128 * A boolean used to track if the keyboard is ready for user input. As
129 * alternate layouts are dynamically loaded, the keyboard may be in a state
130 * where it is not fully initialized until all links, key-sequences, and
131 * imports are fully resolved.
135 // Flag values for ctrl, alt and shift as defined by EventFlags
136 // in "event_constants.h".
146 * The enumeration of swipe directions.
150 var SWIPE_DIRECTION = {
158 * A structure to track the current swipe status.
163 * The count of horizontal and vertical movement.
170 * Last touch coordinate.
177 * The flag of current modifier key.
183 * Current swipe direction.
189 * The number of times we've swiped within a single swipe.
195 * Reset all the values when swipe finished.
197 resetAll: function() {
203 this.swipeDirection = 0;
208 Polymer('kb-keyboard', {
214 lastPressedKey: null,
220 * The default input type to keyboard layout map. The key must be one of
221 * the input box type values.
224 inputTypeToLayoutMap: {
231 * Changes the current keyset.
232 * @param {Object} detail The detail of the event that called this
235 changeKeyset: function(detail) {
236 if (detail.relegateToShift && this.shift) {
237 this.keyset = this.shift.textKeyset;
238 this.activeKeyset.nextKeyset = undefined;
241 var toKeyset = detail.toKeyset;
243 this.keyset = toKeyset;
244 this.activeKeyset.nextKeyset = detail.nextKeyset;
251 this.voiceInput_ = new VoiceInput(this);
252 this.swipeHandler = this.onSwipeUpdate.bind(this);
256 * Called when the type of focused input box changes. If a keyboard layout
257 * is defined for the current input type, that layout will be loaded.
258 * Otherwise, the keyboard layout for 'text' type will be loaded.
260 inputTypeChanged: function() {
261 // TODO(bshe): Toggle visibility of some keys in a keyboard layout
262 // according to the input type.
263 var layout = this.inputTypeToLayoutMap[this.inputType];
265 layout = this.inputTypeToLayoutMap.text;
266 this.layout = layout;
270 * When double click/tap event is enabled, the second key-down and key-up
271 * events on the same key should be skipped. Return true when the event
272 * with |detail| should be skipped.
273 * @param {Object} detail The detail of key-up or key-down event.
275 skipEvent: function(detail) {
276 if (this.dblDetail_) {
277 if (this.dblDetail_.char != detail.char) {
278 // The second key down is not on the same key. Double click/tap
279 // should be ignored.
280 this.dblDetail_ = null;
281 clearTimeout(this.dblTimer_);
282 } else if (this.dblDetail_.clickCount == 1) {
290 * This function is bound to swipeHandler. And swipeHandler handle
291 * the pointermove event after pointerdown event happened.
292 * @para {PointerEvent} event.
294 onSwipeUpdate: function(event) {
295 if (!event.isPrimary)
297 swipeStatus.offset_x += event.screenX - swipeStatus.pre_x;
298 // swipeStatus.offset_y += event.screenY - swipeStatus.pre_y;
299 if (Math.abs(swipeStatus.offset_x) > MIN_SWIPE_DIST ||
300 Math.abs(swipeStatus.offset_y) > MIN_SWIPE_DIST) {
301 swipeInProgress = true;
302 if (this.lastPressedKey) {
303 this.lastPressedKey.classList.remove('active');
304 this.lastPressedKey = null;
307 if (swipeStatus.offset_x > MIN_SWIPE_DIST) {
308 swipeStatus.swipeDirection |= SWIPE_DIRECTION.RIGHT;
309 swipeStatus.swipeIndex++;
310 swipeStatus.offset_x = 0;
311 } else if (swipeStatus.offset_x < -MIN_SWIPE_DIST) {
312 swipeStatus.swipeDirection |= SWIPE_DIRECTION.LEFT;
313 swipeStatus.swipeIndex--;
314 swipeStatus.offset_x = 0;
316 // Swipe vertically only when the swipe reaches the gradient of 45
317 // degree. This can also be larger.
318 if (Math.abs(event.screenY - swipeStatus.pre_y) >
319 Math.abs(event.screenX - swipeStatus.pre_x)) {
320 if (swipeStatus.offset_y > MIN_SWIPE_DIST) {
321 swipeStatus.swipeDirection |= SWIPE_DIRECTION.DOWN;
322 swipeStatus.offset_y = 0;
323 } else if (swipeStatus.offset_y < -MIN_SWIPE_DIST) {
324 swipeStatus.swipeDirection |= SWIPE_DIRECTION.UP;
325 swipeStatus.offset_y = 0;
328 if (swipeStatus.swipeDirection) {
330 if (swipeStatus.swipeIndex % 2 != 0) {
331 modifiers |= Modifier.SHIFT;
332 modifiers |= Modifier.CONTROL;
334 MoveCursor(swipeStatus.swipeDirection, modifiers);
335 swipeStatus.swipeDirection = 0;
337 swipeStatus.pre_x = event.screenX;
338 swipeStatus.pre_y = event.screenY;
342 * Handles key-down event that is sent by kb-key-base.
343 * @param {CustomEvent} event The key-down event dispatched by
345 * @param {Object} detail The detail of pressed kb-key.
347 keyDown: function(event, detail) {
348 if (this.skipEvent(detail))
351 if (this.lastPressedKey) {
352 this.lastPressedKey.classList.remove('active');
353 this.lastPressedKey.autoRelease();
355 this.lastPressedKey = event.target;
356 this.lastPressedKey.classList.add('active');
359 var char = detail.char;
362 this.classList.remove('caps-locked');
366 var modifier = char.toLowerCase() + "-active";
367 // Removes modifier if already active.
368 if (this.classList.contains(modifier))
369 this.classList.remove(modifier);
374 this.shift.onNonControlKeyDown();
376 this.ctrl.onNonControlKeyDown();
378 this.alt.onNonControlKeyDown();
381 if(this.changeKeyset(detail))
384 this.keyTyped(detail);
385 this.onNonControlKeyTyped();
386 repeatKey.key = this.lastPressedKey;
388 repeatKey.timer = setTimeout(function() {
389 repeatKey.timer = undefined;
390 repeatKey.interval = setInterval(function() {
391 self.keyTyped(detail);
392 }, REPEAT_INTERVAL_MSEC);
393 }, Math.max(0, REPEAT_DELAY_MSEC - REPEAT_INTERVAL_MSEC));
398 * Handles key-out event that is sent by kb-shift-key.
399 * @param {CustomEvent} event The key-out event dispatched by
401 * @param {Object} detail The detail of pressed kb-shift-key.
403 keyOut: function(event, detail) {
404 this.changeKeyset(detail);
408 * Enable/start double click/tap event recognition.
409 * @param {CustomEvent} event The enable-dbl event dispatched by
411 * @param {Object} detail The detail of pressed kb-shift-key.
413 enableDbl: function(event, detail) {
414 if (!this.dblDetail_) {
415 this.dblDetail_ = detail;
416 this.dblDetail_.clickCount = 0;
418 this.dblTimer_ = setTimeout(function() {
419 self.dblDetail_.callback = null;
420 self.dblDetail_ = null;
421 }, DBL_INTERVAL_MSEC);
426 * Enable the selection while swipe.
427 * @param {CustomEvent} event The enable-dbl event dispatched by
430 enableSel: function(event) {
431 // TODO(rsadam): Disabled for now. May come back if we revert swipe
432 // selection to not do word selection.
436 * Handles pointerdown event. This is used for swipe selection process.
437 * to get the start pre_x and pre_y. And also add a pointermove handler
438 * to start handling the swipe selection event.
439 * @param {PointerEvent} event The pointerup event that received by
442 down: function(event) {
443 if (event.isPrimary) {
444 swipeStatus.pre_x = event.screenX;
445 swipeStatus.pre_y = event.screenY;
446 this.addEventListener("pointermove", this.swipeHandler, false);
451 * Handles pointerup event. This is used for double tap/click events.
452 * @param {PointerEvent} event The pointerup event that bubbled to
455 up: function(event) {
456 // When touch typing, it is very possible that finger moves slightly out
457 // of the key area before releases. The key should not be dropped in
459 if (this.lastPressedKey &&
460 this.lastPressedKey.pointerId == event.pointerId) {
461 this.lastPressedKey.autoRelease();
464 if (this.dblDetail_) {
465 this.dblDetail_.clickCount++;
466 if (this.dblDetail_.clickCount == 2) {
467 this.dblDetail_.callback();
468 this.changeKeyset(this.dblDetail_);
469 clearTimeout(this.dblTimer_);
471 this.classList.add('caps-locked');
473 this.dblDetail_ = null;
477 // TODO(zyaozhujun): There are some edge cases to deal with later.
478 // (for instance, what if a second finger trigger a down and up
479 // event sequence while swiping).
480 // When pointer up from the screen, a swipe selection session finished,
481 // all the data should be reset to prepare for the next session.
482 if (event.isPrimary && swipeInProgress) {
483 swipeInProgress = false;
484 swipeStatus.resetAll();
486 this.removeEventListener('pointermove', this.swipeHandler, false);
490 * Handles key-up event that is sent by kb-key-base.
491 * @param {CustomEvent} event The key-up event dispatched by kb-key-base.
492 * @param {Object} detail The detail of pressed kb-key.
494 keyUp: function(event, detail) {
495 if (this.skipEvent(detail))
499 if (detail.activeModifier) {
500 var modifier = detail.activeModifier.toLowerCase() + "-active";
501 this.classList.add(modifier);
503 // Adds the current keyboard modifiers to the detail.
505 detail.controlModifier = this.ctrl.isActive();
507 detail.altModifier = this.alt.isActive();
508 if (this.lastPressedKey)
509 this.lastPressedKey.classList.remove('active');
510 // Keyset transition key. This is needed to transition from upper
511 // to lower case when we are not in caps mode, as well as when
512 // we're ending chording.
513 this.changeKeyset(detail);
515 if (this.lastPressedKey &&
516 this.lastPressedKey.charValue != event.target.charValue) {
519 if (repeatKey.key == event.target) {
521 this.lastPressedKey = null;
524 var toLayoutId = detail.toLayout;
525 // Layout transition key.
527 this.layout = toLayoutId;
528 var char = detail.char;
529 if (enterUpperOnSpace) {
530 enterUpperOnSpace = false;
532 // If shift key defined in layout.
534 var shiftDetail = this.shift.onSpaceAfterPunctuation();
535 // Check if transition defined.
536 this.changeKeyset(shiftDetail);
538 console.error('Capitalization on space after punctuation \
539 enabled, but cannot find target keyset.');
543 this.lastPressedKey = null;
549 swipeStatus.swipeFlags = 0;
552 this.voiceInput_.onDown();
557 enterUpperOnSpace = true;
562 if(!this.keyTyped(detail))
564 this.onNonControlKeyTyped();
568 * Handles key-longpress event that is sent by kb-key-base.
569 * @param {CustomEvent} event The key-longpress event dispatched by
571 * @param {Object} detail The detail of pressed key.
573 keyLongpress: function(event, detail) {
574 // If the gesture is long press, remove the pointermove listener.
575 this.removeEventListener('pointermove', this.swipeHandler, false);
576 // Keyset transtion key.
577 if (this.changeKeyset(detail)) {
578 // Locks the keyset before removing active to prevent flicker.
579 this.classList.add('caps-locked');
580 // Makes last pressed key inactive if transit to a new keyset on long
582 if (this.lastPressedKey)
583 this.lastPressedKey.classList.remove('active');
588 * Show menu for selecting a keyboard layout.
589 * @param {!Event} event The triggering event.
590 * @param {{left: number, top: number, width: number}} details Location of
591 * the button that triggered the popup.
593 showOptions: function(event, details) {
594 var overlay = this.$.overlay;
596 console.error('Missing overlay.');
599 var menu = overlay.$.options;
601 console.error('Missing options menu.');
605 overlay.hidden = false;
606 var left = details.left + details.width - menu.clientWidth;
607 var top = details.top - menu.clientHeight;
608 menu.style.left = left + 'px';
609 menu.style.top = top + 'px';
613 * Handler for the 'set-layout' event.
614 * @param {!Event} event The triggering event.
615 * @param {{layout: string}} details Details of the event, which contains
616 * the name of the layout to activate.
618 setLayout: function(event, details) {
619 this.layout = details.layout;
623 * Handles a change in the keyboard layout. Auto-selects the default
624 * keyset for the new layout.
626 layoutChanged: function() {
627 if (!this.selectDefaultKeyset()) {
628 this.isReady = false;
629 this.fire('stateChange', {state: 'loadingKeyset'});
631 // Keyset selection fails if the keysets have not been loaded yet.
632 var keysets = document.querySelector('#' + this.layout);
634 keyboard.appendChild(flattenKeysets(keysets.content));
635 this.selectDefaultKeyset();
637 // Add link for the keysets if missing from the document. Force
638 // a layout change after resolving the import of the link.
639 var query = 'link[id=' + this.layout + ']';
640 if (!document.querySelector(query)) {
641 // Layout has not beeen loaded yet.
642 var link = document.createElement('link');
643 link.id = this.layout;
644 link.setAttribute('rel', 'import');
645 link.setAttribute('href', 'layouts/' + this.layout + '.html');
646 document.head.appendChild(link);
648 // Load content for the new link element.
650 HTMLImports.importer.load(document, function() {
651 HTMLImports.parser.parseLink(link);
652 self.layoutChanged();
660 * Notifies the modifier keys that a non-control key was typed. This
661 * lets them reset sticky behaviour. A non-control key is defined as
662 * any key that is not Control, Alt, or Shift.
664 onNonControlKeyTyped: function() {
666 this.shift.onNonControlKeyTyped();
668 this.ctrl.onNonControlKeyTyped();
670 this.alt.onNonControlKeyTyped();
671 this.classList.remove('ctrl-active');
672 this.classList.remove('alt-active');
676 * Indicate if the keyboard is ready for user input.
684 * Id for the active keyset.
687 get activeKeysetId() {
688 return this.layout + '-' + this.keyset;
692 * The active keyset DOM object.
696 return this.querySelector('#' + this.activeKeysetId);
700 * The current input type.
703 get inputTypeValue() {
708 * Changes the input type if it's different from the current
709 * type, else resets the keyset to the default keyset.
712 set inputTypeValue(value) {
713 if (value == this.inputType)
714 this.selectDefaultKeyset();
716 this.inputType = value;
720 * Generates fabricated key events to simulate typing on a
722 * @param {Object} detail Attributes of the key being typed.
723 * @return {boolean} Whether the key type succeeded.
725 keyTyped: function(detail) {
726 var builder = this.$.keyCodeMetadata;
728 detail.shiftModifier = this.shift.isActive();
730 detail.controlModifier = this.ctrl.isActive();
732 detail.altModifier = this.alt.isActive();
733 var downEvent = builder.createVirtualKeyEvent(detail, "keydown");
735 sendKeyEvent(downEvent);
736 sendKeyEvent(builder.createVirtualKeyEvent(detail, "keyup"));
743 * Selects the default keyset for a layout.
744 * @return {boolean} True if successful. This method can fail if the
745 * keysets corresponding to the layout have not been injected.
747 selectDefaultKeyset: function() {
748 var keysets = this.querySelectorAll('kb-keyset');
749 // Full name of the keyset is of the form 'layout-keyset'.
750 var regex = new RegExp('^' + this.layout + '-(.+)');
751 var keysetsLoaded = false;
752 for (var i = 0; i < keysets.length; i++) {
753 var matches = keysets[i].id.match(regex);
754 if (matches && matches.length == REGEX_MATCH_COUNT) {
755 keysetsLoaded = true;
756 if (keysets[i].isDefault) {
757 this.keyset = matches[REGEX_KEYSET_INDEX];
758 this.classList.remove('caps-locked');
759 this.classList.remove('alt-active');
760 this.classList.remove('ctrl-active');
762 this.shift = this.querySelector('kb-shift-key');
765 // Caches control key.
766 this.ctrl = this.querySelector('kb-modifier-key[char=Ctrl');
770 this.alt = this.querySelector('kb-modifier-key[char=Alt');
774 this.fire('stateChange', {
775 state: 'keysetLoaded',
784 console.error('No default keyset found for ' + this.layout);