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}}"
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">
25 <!-- The ID for a keyset follows the naming convention of combining the
26 -- layout name with a base keyset name. This convention is used to
27 -- allow multiple layouts to be loaded (enablign fast switching) while
28 -- allowing the shift and spacebar keys to be common across multiple
31 <content id="content"></content>
32 <kb-keyboard-overlay id="overlay" hidden></kb-keyboard-overlay>
33 <kb-key-codes id="keyCodeMetadata"></kb-key-codes>
38 * The repeat delay in milliseconds before a key starts repeating. Use the
39 * same rate as Chromebook.
40 * (See chrome/browser/chromeos/language_preferences.cc)
44 var REPEAT_DELAY_MSEC = 500;
47 * The repeat interval or number of milliseconds between subsequent
48 * keypresses. Use the same rate as Chromebook.
52 var REPEAT_INTERVAL_MSEC = 50;
55 * The double click/tap interval.
59 var DBL_INTERVAL_MSEC = 300;
62 * The index of the name of the keyset when searching for all keysets.
66 var REGEX_KEYSET_INDEX = 1;
69 * The integer number of matches when searching for keysets.
73 var REGEX_MATCH_COUNT = 2;
76 * The boolean to decide if keyboard should transit to upper case keyset
77 * when spacebar is pressed. If a closing punctuation is followed by a
78 * spacebar, keyboard should automatically transit to upper case.
81 var enterUpperOnSpace = false;
84 * A structure to track the currently repeating key on the keyboard.
89 * The timer for the delay before repeating behaviour begins.
90 * @type {number|undefined}
95 * The interval timer for issuing keypresses of a repeating key.
96 * @type {number|undefined}
101 * The key which is currently repeating.
102 * @type {BaseKey|undefined}
107 * Cancel the repeat timers of the currently active key.
110 clearTimeout(this.timer);
111 clearInterval(this.interval);
112 this.timer = undefined;
113 this.interval = undefined;
114 this.key = undefined;
119 * The minimum movement interval needed to trigger cursor move on
120 * horizontal and vertical way.
124 var MIN_SWIPE_DIST_X = 50;
125 var MIN_SWIPE_DIST_Y = 20;
128 * The maximum swipe distance that will trigger hintText of a key
133 var MAX_SWIPE_FLICK_DIST = 60;
136 * The boolean to decide if it is swipe in process or finished.
139 var swipeInProgress = false;
141 // Flag values for ctrl, alt and shift as defined by EventFlags
142 // in "event_constants.h".
152 * A structure to track the current swipe status.
156 * The latest PointerMove event in the swipe.
159 currentEvent: undefined,
162 * Whether or not a swipe changes direction.
168 * The count of horizontal and vertical movement.
175 * Last touch coordinate.
182 * The PointerMove event which triggered the swipe.
185 startEvent: undefined,
188 * The flag of current modifier key.
194 * Current swipe direction.
200 * The number of times we've swiped within a single swipe.
206 * Returns the combined direction of the x and y offsets.
207 * @return {number} The latest direction.
209 getOffsetDirection: function() {
210 // TODO (rsadam): Use angles to figure out the direction.
212 // Checks for horizontal swipe.
213 if (Math.abs(this.offset_x) > MIN_SWIPE_DIST_X) {
214 if (this.offset_x > 0) {
215 direction |= SwipeDirection.RIGHT;
217 direction |= SwipeDirection.LEFT;
220 // Checks for vertical swipe.
221 if (Math.abs(this.offset_y) > MIN_SWIPE_DIST_Y) {
222 if (this.offset_y < 0) {
223 direction |= SwipeDirection.UP;
225 direction |= SwipeDirection.DOWN;
232 * Populates the swipe update details.
233 * @param {boolean} endSwipe Whether this is the final event for this
235 * @return {Object} The current state of the swipeTracker.
237 populateDetails: function(endSwipe) {
239 detail.direction = this.swipeDirection;
240 detail.index = this.swipeIndex;
241 detail.status = this.swipeStatus;
242 detail.endSwipe = endSwipe;
243 detail.startEvent = this.startEvent;
244 detail.currentEvent = this.currentEvent;
245 detail.isComplex = this.isComplex;
250 * Reset all the values when swipe finished.
252 resetAll: function() {
258 this.swipeDirection = 0;
260 this.startEvent = undefined;
261 this.currentEvent = undefined;
262 this.isComplex = false;
266 * Updates the swipe path with the current event.
267 * @param {Object} event The PointerEvent that triggered this update.
268 * @return {boolean} Whether or not to notify swipe observers.
270 update: function(event) {
274 this.offset_x += event.screenX - this.pre_x;
275 this.offset_y += event.screenY - this.pre_y;
276 this.pre_x = event.screenX;
277 this.pre_y = event.screenY;
279 // Check if movement crosses minimum thresholds in each direction.
280 var direction = this.getOffsetDirection();
283 // If swipeIndex is zero the current event is triggering the swipe.
284 if (this.swipeIndex == 0) {
285 this.startEvent = event;
286 } else if (direction != this.swipeDirection) {
287 // Toggle the isComplex flag.
288 this.isComplex = true;
290 // Update the swipe tracker.
291 this.swipeDirection = direction;
294 this.currentEvent = event;
301 Polymer('kb-keyboard', {
308 lastPressedKey: null,
315 * The default input type to keyboard layout map. The key must be one of
316 * the input box type values.
319 inputTypeToLayoutMap: {
326 * Changes the current keyset.
327 * @param {Object} detail The detail of the event that called this
330 changeKeyset: function(detail) {
331 if (detail.relegateToShift && this.shift) {
332 this.keyset = this.shift.textKeyset;
333 this.activeKeyset.nextKeyset = undefined;
336 var toKeyset = detail.toKeyset;
338 this.keyset = toKeyset;
339 this.activeKeyset.nextKeyset = detail.nextKeyset;
345 keysetChanged: function() {
346 var keyset = this.activeKeyset;
347 // Show the keyset if it has been initialized.
352 configChanged: function() {
353 this.layout = this.config.layout;
357 this.voiceInput_ = new VoiceInput(this);
358 this.swipeHandler = this.move.bind(this);
360 getKeyboardConfig(function(config) {
361 self.config = config;
366 * Registers a callback for state change events.
367 * @param{!Function} callback Callback function to register.
369 addKeysetChangedObserver: function(callback) {
370 this.addEventListener('stateChange', callback);
374 * Called when the type of focused input box changes. If a keyboard layout
375 * is defined for the current input type, that layout will be loaded.
376 * Otherwise, the keyboard layout for 'text' type will be loaded.
378 inputTypeChanged: function() {
379 // Disable layout switching at accessbility mode.
380 if (this.config && this.config.a11ymode)
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];
387 layout = this.inputTypeToLayoutMap.text;
388 this.layout = layout;
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.
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) {
412 * Handles a swipe update.
413 * param {Object} detail The swipe update details.
415 onSwipeUpdate: function(detail) {
416 var direction = detail.direction;
418 console.error("Swipe direction cannot be: " + direction);
419 // Triggers swipe editting if it's a purely horizontal swipe.
420 if (!(direction & (SwipeDirection.UP | SwipeDirection.DOWN))) {
421 // Nothing to do if the swipe has ended.
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;
431 MoveCursor(direction, modifiers);
434 // Triggers swipe hintText if it's a purely vertical swipe.
435 if (!(direction & (SwipeDirection.LEFT | SwipeDirection.RIGHT))) {
436 // Check if event is relevant to us.
437 if ((!detail.endSwipe) || (detail.isComplex))
440 var distance = Math.abs(detail.startEvent.screenY -
441 detail.currentEvent.screenY);
442 if (distance > MAX_SWIPE_FLICK_DIST)
444 var triggerKey = detail.startEvent.target;
445 if (triggerKey && triggerKey.onFlick)
446 triggerKey.onFlick(detail);
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.
455 move: function(event) {
456 if (!swipeTracker.update(event))
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;
464 this.onSwipeUpdate(swipeTracker.populateDetails(false));
468 * Handles key-down event that is sent by kb-key-base.
469 * @param {CustomEvent} event The key-down event dispatched by
471 * @param {Object} detail The detail of pressed kb-key.
473 keyDown: function(event, detail) {
474 if (this.skipEvent(detail))
477 if (this.lastPressedKey) {
478 this.lastPressedKey.classList.remove('active');
479 this.lastPressedKey.autoRelease();
481 this.lastPressedKey = event.target;
482 this.lastPressedKey.classList.add('active');
485 var char = detail.char;
488 this.classList.remove('caps-locked');
492 var modifier = char.toLowerCase() + "-active";
493 // Removes modifier if already active.
494 if (this.classList.contains(modifier))
495 this.classList.remove(modifier);
498 // Not all Invalid keys are transition keys. Reset control keys if
499 // we pressed a transition key.
500 if (event.target.toKeyset || detail.relegateToShift)
501 this.onNonControlKeyTyped();
506 this.shift.onNonControlKeyDown();
508 this.ctrl.onNonControlKeyDown();
510 this.alt.onNonControlKeyDown();
513 if(this.changeKeyset(detail))
516 this.keyTyped(detail);
517 this.onNonControlKeyTyped();
518 repeatKey.key = this.lastPressedKey;
520 repeatKey.timer = setTimeout(function() {
521 repeatKey.timer = undefined;
522 repeatKey.interval = setInterval(function() {
523 self.keyTyped(detail);
524 }, REPEAT_INTERVAL_MSEC);
525 }, Math.max(0, REPEAT_DELAY_MSEC - REPEAT_INTERVAL_MSEC));
530 * Handles key-out event that is sent by kb-shift-key.
531 * @param {CustomEvent} event The key-out event dispatched by
533 * @param {Object} detail The detail of pressed kb-shift-key.
535 keyOut: function(event, detail) {
536 this.changeKeyset(detail);
540 * Enable/start double click/tap event recognition.
541 * @param {CustomEvent} event The enable-dbl event dispatched by
543 * @param {Object} detail The detail of pressed kb-shift-key.
545 enableDbl: function(event, detail) {
546 if (!this.dblDetail_) {
547 this.dblDetail_ = detail;
548 this.dblDetail_.clickCount = 0;
550 this.dblTimer_ = setTimeout(function() {
551 self.dblDetail_.callback = null;
552 self.dblDetail_ = null;
553 }, DBL_INTERVAL_MSEC);
558 * Enable the selection while swipe.
559 * @param {CustomEvent} event The enable-dbl event dispatched by
562 enableSel: function(event) {
563 // TODO(rsadam): Disabled for now. May come back if we revert swipe
564 // selection to not do word selection.
568 * Handles pointerdown event. This is used for swipe selection process.
569 * to get the start pre_x and pre_y. And also add a pointermove handler
570 * to start handling the swipe selection event.
571 * @param {PointerEvent} event The pointerup event that received by
574 down: function(event) {
575 if (event.isPrimary) {
576 swipeTracker.pre_x = event.screenX;
577 swipeTracker.pre_y = event.screenY;
578 this.addEventListener("pointermove", this.swipeHandler, false);
583 * Handles pointerup event. This is used for double tap/click events.
584 * @param {PointerEvent} event The pointerup event that bubbled to
587 up: function(event) {
588 // When touch typing, it is very possible that finger moves slightly out
589 // of the key area before releases. The key should not be dropped in
591 if (this.lastPressedKey &&
592 this.lastPressedKey.pointerId == event.pointerId) {
593 this.lastPressedKey.autoRelease();
596 if (this.dblDetail_) {
597 this.dblDetail_.clickCount++;
598 if (this.dblDetail_.clickCount == 2) {
599 this.dblDetail_.callback();
600 this.changeKeyset(this.dblDetail_);
601 clearTimeout(this.dblTimer_);
603 this.classList.add('caps-locked');
605 this.dblDetail_ = null;
609 // TODO(zyaozhujun): There are some edge cases to deal with later.
610 // (for instance, what if a second finger trigger a down and up
611 // event sequence while swiping).
612 // When pointer up from the screen, a swipe selection session finished,
613 // all the data should be reset to prepare for the next session.
614 if (event.isPrimary && swipeInProgress) {
615 swipeInProgress = false;
616 this.onSwipeUpdate(swipeTracker.populateDetails(true))
617 swipeTracker.resetAll();
619 this.removeEventListener('pointermove', this.swipeHandler, false);
623 * Handles PointerOut event. This is used for when a swipe gesture goes
624 * outside of the keyboard window.
625 * @param {Object} event The pointerout event that bubbled to the
628 out: function(event) {
629 // Ignore if triggered from one of the keys.
630 if (this.compareDocumentPosition(event.relatedTarget) &
631 Node.DOCUMENT_POSITION_CONTAINED_BY)
634 this.onSwipeUpdate(swipeTracker.populateDetails(true))
635 // Touched outside of the keyboard area, so disables swipe.
636 swipeInProgress = false;
637 swipeTracker.resetAll();
638 this.removeEventListener('pointermove', this.swipeHandler, false);
642 * Handles a TypeKey event. This is used for when we programmatically
643 * want to type a specific key.
644 * @param {CustomEvent} event The TypeKey event that bubbled to the
647 type: function(event) {
648 this.keyTyped(event.detail);
652 * Handles key-up event that is sent by kb-key-base.
653 * @param {CustomEvent} event The key-up event dispatched by kb-key-base.
654 * @param {Object} detail The detail of pressed kb-key.
656 keyUp: function(event, detail) {
657 if (this.skipEvent(detail))
661 if (detail.activeModifier) {
662 var modifier = detail.activeModifier.toLowerCase() + "-active";
663 this.classList.add(modifier);
665 // Adds the current keyboard modifiers to the detail.
667 detail.controlModifier = this.ctrl.isActive();
669 detail.altModifier = this.alt.isActive();
670 if (this.lastPressedKey)
671 this.lastPressedKey.classList.remove('active');
672 // Keyset transition key. This is needed to transition from upper
673 // to lower case when we are not in caps mode, as well as when
674 // we're ending chording.
675 this.changeKeyset(detail);
677 if (this.lastPressedKey &&
678 this.lastPressedKey.charValue != event.target.charValue) {
681 if (repeatKey.key == event.target) {
683 this.lastPressedKey = null;
686 var toLayoutId = detail.toLayout;
687 // Layout transition key.
689 this.layout = toLayoutId;
690 var char = detail.char;
691 this.lastPressedKey = null;
692 // Characters that should not be typed.
698 enterUpperOnSpace = false;
699 swipeTracker.swipeFlags = 0;
702 this.voiceInput_.onDown();
707 // Tries to type the character. Resorts to insertText if that fails.
708 if(!this.keyTyped(detail))
710 // Post-typing logic.
713 if(enterUpperOnSpace) {
714 enterUpperOnSpace = false;
716 var shiftDetail = this.shift.onSpaceAfterPunctuation();
717 // Check if transition defined.
718 this.changeKeyset(shiftDetail);
720 console.error('Capitalization on space after punctuation \
721 enabled, but cannot find target keyset.');
723 // Immediately return to maintain shift-state. Space is a
724 // non-control key and would otherwise trigger a reset of the
725 // shift key, causing a transition to lower case.
726 // TODO(rsadam): Add unit test after Polymer uprev complete.
733 enterUpperOnSpace = this.shouldUpperOnSpace();
738 // Reset control keys.
739 this.onNonControlKeyTyped();
743 * Handles key-longpress event that is sent by kb-key-base.
744 * @param {CustomEvent} event The key-longpress event dispatched by
746 * @param {Object} detail The detail of pressed key.
748 keyLongpress: function(event, detail) {
749 // If the gesture is long press, remove the pointermove listener.
750 this.removeEventListener('pointermove', this.swipeHandler, false);
751 // Keyset transtion key.
752 if (this.changeKeyset(detail)) {
753 // Locks the keyset before removing active to prevent flicker.
754 this.classList.add('caps-locked');
755 // Makes last pressed key inactive if transit to a new keyset on long
757 if (this.lastPressedKey)
758 this.lastPressedKey.classList.remove('active');
763 * Whether we should transit to upper case when seeing a space after
767 shouldUpperOnSpace: function() {
768 // TODO(rsadam): Add other input types in which we should not
769 // transition to upper after a space.
770 return this.inputTypeValue != 'password';
774 * Show menu for selecting a keyboard layout.
775 * @param {!Event} event The triggering event.
776 * @param {{left: number, top: number, width: number}} details Location of
777 * the button that triggered the popup.
779 showOptions: function(event, details) {
780 var overlay = this.$.overlay;
782 console.error('Missing overlay.');
785 var menu = overlay.$.options;
787 console.error('Missing options menu.');
790 overlay.hidden = false;
791 var left = details.left + details.width - menu.clientWidth;
792 var top = details.top - menu.clientHeight;
793 menu.style.left = left + 'px';
794 menu.style.top = top + 'px';
798 * Handler for the 'set-layout' event.
799 * @param {!Event} event The triggering event.
800 * @param {{layout: string}} details Details of the event, which contains
801 * the name of the layout to activate.
803 setLayout: function(event, details) {
804 this.layout = details.layout;
808 * Handles a change in the keyboard layout. Auto-selects the default
809 * keyset for the new layout.
811 layoutChanged: function() {
813 if (!this.selectDefaultKeyset()) {
814 this.fire('stateChange', {state: 'loadingKeyset'});
816 // Keyset selection fails if the keysets have not been loaded yet.
817 var keysets = document.querySelector('#' + this.layout);
818 if (keysets && keysets.content) {
819 var content = flattenKeysets(keysets.content);
820 this.appendChild(content);
821 this.selectDefaultKeyset();
823 // Add link for the keysets if missing from the document. Force
824 // a layout change after resolving the import of the link.
825 var query = 'link[id=' + this.layout + ']';
826 if (!document.querySelector(query)) {
827 // Layout has not beeen loaded yet.
828 var link = document.createElement('link');
829 link.id = this.layout;
830 link.setAttribute('rel', 'import');
831 link.setAttribute('href', 'layouts/' + this.layout + '.html');
832 document.head.appendChild(link);
834 // Load content for the new link element.
836 HTMLImports.importer.load(document, function() {
837 HTMLImports.parser.parseLink(link);
838 self.layoutChanged();
842 // New keyset has already been loaded, can show immediately.
844 this.activeKeyset.show();
849 * Notifies the modifier keys that a non-control key was typed. This
850 * lets them reset sticky behaviour. A non-control key is defined as
851 * any key that is not Control, Alt, or Shift.
853 onNonControlKeyTyped: function() {
855 this.shift.onNonControlKeyTyped();
857 this.ctrl.onNonControlKeyTyped();
859 this.alt.onNonControlKeyTyped();
860 this.classList.remove('ctrl-active');
861 this.classList.remove('alt-active');
865 * Id for the active keyset.
868 get activeKeysetId() {
869 return this.layout + '-' + this.keyset;
873 * The active keyset DOM object.
877 return this.querySelector('#' + this.activeKeysetId);
881 * The current input type.
884 get inputTypeValue() {
885 return this.inputType;
889 * Changes the input type if it's different from the current
890 * type, else resets the keyset to the default keyset.
893 set inputTypeValue(value) {
894 if (value == this.inputType)
895 this.selectDefaultKeyset();
897 this.inputType = value;
901 * The keyboard is ready for input once the target keyset appears
902 * in the distributed nodes for the keyboard.
903 * @return {boolean} Indicates if the keyboard is ready for input.
905 isReady: function() {
906 var keyset = this.activeKeyset;
909 var nodes = this.$.content.getDistributedNodes();
910 for (var i = 0; i < nodes.length; i++) {
911 if (nodes[i].id && nodes[i].id == keyset.id)
918 * Generates fabricated key events to simulate typing on a
920 * @param {Object} detail Attributes of the key being typed.
921 * @return {boolean} Whether the key type succeeded.
923 keyTyped: function(detail) {
924 var builder = this.$.keyCodeMetadata;
926 detail.shiftModifier = this.shift.isActive();
928 detail.controlModifier = this.ctrl.isActive();
930 detail.altModifier = this.alt.isActive();
931 var downEvent = builder.createVirtualKeyEvent(detail, "keydown");
933 sendKeyEvent(downEvent);
934 sendKeyEvent(builder.createVirtualKeyEvent(detail, "keyup"));
941 * Selects the default keyset for a layout.
942 * @return {boolean} True if successful. This method can fail if the
943 * keysets corresponding to the layout have not been injected.
945 selectDefaultKeyset: function() {
946 var keysets = this.querySelectorAll('kb-keyset');
947 // Full name of the keyset is of the form 'layout-keyset'.
948 var regex = new RegExp('^' + this.layout + '-(.+)');
949 var keysetsLoaded = false;
950 for (var i = 0; i < keysets.length; i++) {
951 var matches = keysets[i].id.match(regex);
952 if (matches && matches.length == REGEX_MATCH_COUNT) {
953 keysetsLoaded = true;
954 // Without both tests for a default keyset, it is possible to get
955 // into a state where multiple layouts are displayed. A
956 // reproducable test case is do the following set of keyset
957 // transitions: qwerty -> system -> dvorak -> qwerty.
958 // TODO(kevers): Investigate why this is the case.
959 if (keysets[i].isDefault ||
960 keysets[i].getAttribute('isDefault') == 'true') {
961 this.keyset = matches[REGEX_KEYSET_INDEX];
962 this.classList.remove('caps-locked');
963 this.classList.remove('alt-active');
964 this.classList.remove('ctrl-active');
966 this.shift = this.querySelector('kb-shift-key');
969 // Caches control key.
970 this.ctrl = this.querySelector('kb-modifier-key[char=Ctrl]');
974 this.alt = this.querySelector('kb-modifier-key[char=Alt]');
977 this.fire('stateChange', {
978 state: 'keysetLoaded',
987 console.error('No default keyset found for ' + this.layout);