| <!-- |
| -- Copyright 2013 The Chromium Authors. All rights reserved. |
| -- Use of this source code is governed by a BSD-style license that can be |
| -- found in the LICENSE file. |
| --> |
| |
| <polymer-element name="kb-keyboard" on-key-over="{{keyOver}}" |
| on-key-up="{{keyUp}}" on-key-down="{{keyDown}}" |
| on-key-longpress="{{keyLongpress}}" on-pointerup="{{up}}" |
| on-pointerdown="{{down}}" on-pointerout="{{out}}" |
| on-enable-sel="{{enableSel}}" on-enable-dbl="{{enableDbl}}" |
| on-key-out="{{keyOut}}" on-show-options="{{showOptions}}" |
| on-set-layout="{{setLayout}}" |
| attributes="keyset layout inputType inputTypeToLayoutMap"> |
| <template> |
| <style> |
| @host { |
| * { |
| position: relative; |
| } |
| } |
| </style> |
| <!-- The ID for a keyset follows the naming convention of combining the |
| -- layout name with a base keyset name. This convention is used to |
| -- allow multiple layouts to be loaded (enablign fast switching) while |
| -- allowing the shift and spacebar keys to be common across multiple |
| -- keyboard layouts. |
| --> |
| <content id="content" select="#{{layout}}-{{keyset}}"></content> |
| <kb-keyboard-overlay id="overlay" hidden></kb-keyboard-overlay> |
| <kb-key-codes id="keyCodeMetadata"></kb-key-codes> |
| </template> |
| <script> |
| /** |
| * The repeat delay in milliseconds before a key starts repeating. Use the |
| * same rate as Chromebook. |
| * (See chrome/browser/chromeos/language_preferences.cc) |
| * @const |
| * @type {number} |
| */ |
| var REPEAT_DELAY_MSEC = 500; |
| |
| /** |
| * The repeat interval or number of milliseconds between subsequent |
| * keypresses. Use the same rate as Chromebook. |
| * @const |
| * @type {number} |
| */ |
| var REPEAT_INTERVAL_MSEC = 50; |
| |
| /** |
| * The double click/tap interval. |
| * @const |
| * @type {number} |
| */ |
| var DBL_INTERVAL_MSEC = 300; |
| |
| /** |
| * The index of the name of the keyset when searching for all keysets. |
| * @const |
| * @type {number} |
| */ |
| var REGEX_KEYSET_INDEX = 1; |
| |
| /** |
| * The integer number of matches when searching for keysets. |
| * @const |
| * @type {number} |
| */ |
| var REGEX_MATCH_COUNT = 2; |
| |
| /** |
| * The boolean to decide if keyboard should transit to upper case keyset |
| * when spacebar is pressed. If a closing punctuation is followed by a |
| * spacebar, keyboard should automatically transit to upper case. |
| * @type {boolean} |
| */ |
| var enterUpperOnSpace = false; |
| |
| /** |
| * A structure to track the currently repeating key on the keyboard. |
| */ |
| var repeatKey = { |
| |
| /** |
| * The timer for the delay before repeating behaviour begins. |
| * @type {number|undefined} |
| */ |
| timer: undefined, |
| |
| /** |
| * The interval timer for issuing keypresses of a repeating key. |
| * @type {number|undefined} |
| */ |
| interval: undefined, |
| |
| /** |
| * The key which is currently repeating. |
| * @type {BaseKey|undefined} |
| */ |
| key: undefined, |
| |
| /** |
| * Cancel the repeat timers of the currently active key. |
| */ |
| cancel: function() { |
| clearTimeout(this.timer); |
| clearInterval(this.interval); |
| this.timer = undefined; |
| this.interval = undefined; |
| this.key = undefined; |
| } |
| }; |
| |
| /** |
| * The minimum movement interval needed to trigger cursor move on |
| * horizontal and vertical way. |
| * @const |
| * @type {number} |
| */ |
| var MIN_SWIPE_DIST_X = 50; |
| var MIN_SWIPE_DIST_Y = 20; |
| |
| /** |
| * The maximum swipe distance that will trigger hintText of a key |
| * to be typed. |
| * @const |
| * @type {number} |
| */ |
| var MAX_SWIPE_FLICK_DIST = 60; |
| |
| /** |
| * The boolean to decide if it is swipe in process or finished. |
| * @type {boolean} |
| */ |
| var swipeInProgress = false; |
| |
| // Flag values for ctrl, alt and shift as defined by EventFlags |
| // in "event_constants.h". |
| // @enum {number} |
| var Modifier = { |
| NONE: 0, |
| ALT: 8, |
| CONTROL: 4, |
| SHIFT: 2 |
| }; |
| |
| /** |
| * The enumeration of swipe directions. |
| * @const |
| * @type {Enum} |
| */ |
| var SWIPE_DIRECTION = { |
| RIGHT: 0x1, |
| LEFT: 0x2, |
| UP: 0x4, |
| DOWN: 0x8 |
| }; |
| |
| /** |
| * A structure to track the current swipe status. |
| */ |
| var swipeTracker = { |
| /** |
| * The latest PointerMove event in the swipe. |
| * @type {Object} |
| */ |
| currentEvent: undefined, |
| |
| /** |
| * Whether or not a swipe changes direction. |
| * @type {false} |
| */ |
| isComplex: false, |
| |
| /** |
| * The count of horizontal and vertical movement. |
| * @type {number} |
| */ |
| offset_x : 0, |
| offset_y : 0, |
| |
| /** |
| * Last touch coordinate. |
| * @type {number} |
| */ |
| pre_x : 0, |
| pre_y : 0, |
| |
| /** |
| * The PointerMove event which triggered the swipe. |
| * @type {Object} |
| */ |
| startEvent: undefined, |
| |
| /** |
| * The flag of current modifier key. |
| * @type {number} |
| */ |
| swipeFlags : 0, |
| |
| /** |
| * Current swipe direction. |
| * @type {number} |
| */ |
| swipeDirection : 0, |
| |
| /** |
| * The number of times we've swiped within a single swipe. |
| * @type {number} |
| */ |
| swipeIndex: 0, |
| |
| /** |
| * Returns the combined direction of the x and y offsets. |
| * @return {number} The latest direction. |
| */ |
| getOffsetDirection: function() { |
| // TODO (rsadam): Use angles to figure out the direction. |
| var direction = 0; |
| // Checks for horizontal swipe. |
| if (Math.abs(this.offset_x) > MIN_SWIPE_DIST_X) { |
| if (this.offset_x > 0) { |
| direction |= SWIPE_DIRECTION.RIGHT; |
| } else { |
| direction |= SWIPE_DIRECTION.LEFT; |
| } |
| } |
| // Checks for vertical swipe. |
| if (Math.abs(this.offset_y) > MIN_SWIPE_DIST_Y) { |
| if (this.offset_y < 0) { |
| direction |= SWIPE_DIRECTION.UP; |
| } else { |
| direction |= SWIPE_DIRECTION.DOWN; |
| } |
| } |
| return direction; |
| }, |
| |
| /** |
| * Populates the swipe update details. |
| * @param {boolean} endSwipe Whether this is the final event for this |
| * swipe. |
| * @return {Object} The current state of the swipeTracker. |
| */ |
| populateDetails: function(endSwipe) { |
| var detail = {}; |
| detail.direction = this.swipeDirection; |
| detail.index = this.swipeIndex; |
| detail.status = this.swipeStatus; |
| detail.endSwipe = endSwipe; |
| detail.startEvent = this.startEvent; |
| detail.currentEvent = this.currentEvent; |
| detail.isComplex = this.isComplex; |
| return detail; |
| }, |
| |
| /** |
| * Reset all the values when swipe finished. |
| */ |
| resetAll: function() { |
| this.offset_x = 0; |
| this.offset_y = 0; |
| this.pre_x = 0; |
| this.pre_y = 0; |
| this.swipeFlags = 0; |
| this.swipeDirection = 0; |
| this.swipeIndex = 0; |
| this.startEvent = undefined; |
| this.currentEvent = undefined; |
| this.isComplex = false; |
| }, |
| |
| /** |
| * Updates the swipe path with the current event. |
| * @param {Object} event The PointerEvent that triggered this update. |
| * @return {boolean} Whether or not to notify swipe observers. |
| */ |
| update: function(event) { |
| if(!event.isPrimary) |
| return false; |
| // Update priors. |
| this.offset_x += event.screenX - this.pre_x; |
| this.offset_y += event.screenY - this.pre_y; |
| this.pre_x = event.screenX; |
| this.pre_y = event.screenY; |
| |
| // Check if movement crosses minimum thresholds in each direction. |
| var direction = this.getOffsetDirection(); |
| if (direction == 0) |
| return false; |
| // If swipeIndex is zero the current event is triggering the swipe. |
| if (this.swipeIndex == 0) { |
| this.startEvent = event; |
| } else if (direction != this.swipeDirection) { |
| // Toggle the isComplex flag. |
| this.isComplex = true; |
| } |
| // Update the swipe tracker. |
| this.swipeDirection = direction; |
| this.offset_x = 0; |
| this.offset_y = 0; |
| this.currentEvent = event; |
| this.swipeIndex++; |
| return true; |
| }, |
| |
| }; |
| |
| Polymer('kb-keyboard', { |
| alt: null, |
| control: null, |
| dblDetail_: null, |
| dblTimer_: null, |
| inputType: null, |
| lastPressedKey: null, |
| shift: null, |
| swipeHandler: null, |
| voiceInput_: null, |
| |
| /** |
| * The default input type to keyboard layout map. The key must be one of |
| * the input box type values. |
| * @type {object} |
| */ |
| inputTypeToLayoutMap: { |
| number: "numeric", |
| text: "qwerty", |
| password: "qwerty" |
| }, |
| |
| /** |
| * Changes the current keyset. |
| * @param {Object} detail The detail of the event that called this |
| * function. |
| */ |
| changeKeyset: function(detail) { |
| if (detail.relegateToShift && this.shift) { |
| this.keyset = this.shift.textKeyset; |
| this.activeKeyset.nextKeyset = undefined; |
| return true; |
| } |
| var toKeyset = detail.toKeyset; |
| if (toKeyset) { |
| this.keyset = toKeyset; |
| this.activeKeyset.nextKeyset = detail.nextKeyset; |
| return true; |
| } |
| return false; |
| }, |
| |
| ready: function() { |
| this.voiceInput_ = new VoiceInput(this); |
| this.swipeHandler = this.move.bind(this); |
| }, |
| |
| /** |
| * Registers a callback for state change events. Lazy initializes a |
| * mutation observer used to detect when the keyset selection is changed. |
| * @param{!Function} callback Callback function to register. |
| */ |
| addKeysetChangedObserver: function(callback) { |
| if (!this.keysetChangedObserver) { |
| var target = this.$.content; |
| var self = this; |
| var observer = new MutationObserver(function(mutations) { |
| mutations.forEach(function(m) { |
| if (m.type == 'attributes' && m.attributeName == 'select') { |
| var value = m.target.getAttribute('select'); |
| self.fire('stateChange', { |
| state: 'keysetChanged', |
| value: value |
| }); |
| } |
| }); |
| }); |
| |
| observer.observe(target, { |
| attributes: true, |
| childList: true, |
| subtree: true |
| }); |
| this.keysetChangedObserver = observer; |
| |
| } |
| this.addEventListener('stateChange', callback); |
| }, |
| |
| /** |
| * Called when the type of focused input box changes. If a keyboard layout |
| * is defined for the current input type, that layout will be loaded. |
| * Otherwise, the keyboard layout for 'text' type will be loaded. |
| */ |
| inputTypeChanged: function() { |
| // TODO(bshe): Toggle visibility of some keys in a keyboard layout |
| // according to the input type. |
| var layout = this.inputTypeToLayoutMap[this.inputType]; |
| if (!layout) |
| layout = this.inputTypeToLayoutMap.text; |
| this.layout = layout; |
| }, |
| |
| /** |
| * When double click/tap event is enabled, the second key-down and key-up |
| * events on the same key should be skipped. Return true when the event |
| * with |detail| should be skipped. |
| * @param {Object} detail The detail of key-up or key-down event. |
| */ |
| skipEvent: function(detail) { |
| if (this.dblDetail_) { |
| if (this.dblDetail_.char != detail.char) { |
| // The second key down is not on the same key. Double click/tap |
| // should be ignored. |
| this.dblDetail_ = null; |
| clearTimeout(this.dblTimer_); |
| } else if (this.dblDetail_.clickCount == 1) { |
| return true; |
| } |
| } |
| return false; |
| }, |
| |
| /** |
| * Handles a swipe update. |
| * param {Object} detail The swipe update details. |
| */ |
| onSwipeUpdate: function(detail) { |
| var direction = detail.direction; |
| if (!direction) |
| console.error("Swipe direction cannot be: " + direction); |
| // Triggers swipe editting if it's a purely horizontal swipe. |
| if (!(direction & (SWIPE_DIRECTION.UP | SWIPE_DIRECTION.DOWN))) { |
| // Nothing to do if the swipe has ended. |
| if (detail.endSwipe) |
| return; |
| var modifiers = 0; |
| // TODO (rsadam): This doesn't take into account index shifts caused |
| // by vertical swipes. |
| if (detail.index % 2 != 0) { |
| modifiers |= Modifier.SHIFT; |
| modifiers |= Modifier.CONTROL; |
| } |
| MoveCursor(direction, modifiers); |
| return; |
| } |
| // Triggers swipe hintText if it's a purely vertical swipe. |
| if (!(direction & (SWIPE_DIRECTION.LEFT | SWIPE_DIRECTION.RIGHT))) { |
| // Check if event is relevant to us. |
| if ((!detail.endSwipe) || |
| (direction & SWIPE_DIRECTION.DOWN) || |
| (detail.isComplex)) { |
| return; |
| } |
| // Too long a swipe. |
| var distance = Math.abs(detail.startEvent.screenY - |
| detail.currentEvent.screenY); |
| if (distance > MAX_SWIPE_FLICK_DIST) |
| return; |
| var triggerKey = detail.startEvent.target; |
| // Swipe-up on a key without hintText. |
| if (!triggerKey || !triggerKey.hintTextValue) |
| return; |
| var detail = {}; |
| detail.char = triggerKey.hintTextValue; |
| this.keyTyped(detail); |
| return; |
| } |
| }, |
| |
| /** |
| * This function is bound to swipeHandler. Updates the current swipe |
| * status so that PointerEvents can be converted to Swipe events. |
| * @param {PointerEvent} event. |
| */ |
| move: function(event) { |
| if (!swipeTracker.update(event)) |
| return; |
| // Conversion was successful, swipe is now in progress. |
| swipeInProgress = true; |
| if (this.lastPressedKey) { |
| this.lastPressedKey.classList.remove('active'); |
| this.lastPressedKey = null; |
| } |
| this.onSwipeUpdate(swipeTracker.populateDetails(false)); |
| }, |
| |
| /** |
| * Handles key-down event that is sent by kb-key-base. |
| * @param {CustomEvent} event The key-down event dispatched by |
| * kb-key-base. |
| * @param {Object} detail The detail of pressed kb-key. |
| */ |
| keyDown: function(event, detail) { |
| if (this.skipEvent(detail)) |
| return; |
| |
| if (this.lastPressedKey) { |
| this.lastPressedKey.classList.remove('active'); |
| this.lastPressedKey.autoRelease(); |
| } |
| this.lastPressedKey = event.target; |
| this.lastPressedKey.classList.add('active'); |
| repeatKey.cancel(); |
| |
| var char = detail.char; |
| switch(char) { |
| case 'Shift': |
| this.classList.remove('caps-locked'); |
| break; |
| case 'Alt': |
| case 'Ctrl': |
| var modifier = char.toLowerCase() + "-active"; |
| // Removes modifier if already active. |
| if (this.classList.contains(modifier)) |
| this.classList.remove(modifier); |
| break; |
| default: |
| // Notify shift key. |
| if (this.shift) |
| this.shift.onNonControlKeyDown(); |
| if (this.ctrl) |
| this.ctrl.onNonControlKeyDown(); |
| if (this.alt) |
| this.alt.onNonControlKeyDown(); |
| break; |
| } |
| if(this.changeKeyset(detail)) |
| return; |
| if (detail.repeat) { |
| this.keyTyped(detail); |
| this.onNonControlKeyTyped(); |
| repeatKey.key = this.lastPressedKey; |
| var self = this; |
| repeatKey.timer = setTimeout(function() { |
| repeatKey.timer = undefined; |
| repeatKey.interval = setInterval(function() { |
| self.keyTyped(detail); |
| }, REPEAT_INTERVAL_MSEC); |
| }, Math.max(0, REPEAT_DELAY_MSEC - REPEAT_INTERVAL_MSEC)); |
| } |
| }, |
| |
| /** |
| * Handles key-out event that is sent by kb-shift-key. |
| * @param {CustomEvent} event The key-out event dispatched by |
| * kb-shift-key. |
| * @param {Object} detail The detail of pressed kb-shift-key. |
| */ |
| keyOut: function(event, detail) { |
| this.changeKeyset(detail); |
| }, |
| |
| /** |
| * Enable/start double click/tap event recognition. |
| * @param {CustomEvent} event The enable-dbl event dispatched by |
| * kb-shift-key. |
| * @param {Object} detail The detail of pressed kb-shift-key. |
| */ |
| enableDbl: function(event, detail) { |
| if (!this.dblDetail_) { |
| this.dblDetail_ = detail; |
| this.dblDetail_.clickCount = 0; |
| var self = this; |
| this.dblTimer_ = setTimeout(function() { |
| self.dblDetail_.callback = null; |
| self.dblDetail_ = null; |
| }, DBL_INTERVAL_MSEC); |
| } |
| }, |
| |
| /** |
| * Enable the selection while swipe. |
| * @param {CustomEvent} event The enable-dbl event dispatched by |
| * kb-shift-key. |
| */ |
| enableSel: function(event) { |
| // TODO(rsadam): Disabled for now. May come back if we revert swipe |
| // selection to not do word selection. |
| }, |
| |
| /** |
| * Handles pointerdown event. This is used for swipe selection process. |
| * to get the start pre_x and pre_y. And also add a pointermove handler |
| * to start handling the swipe selection event. |
| * @param {PointerEvent} event The pointerup event that received by |
| * kb-keyboard. |
| */ |
| down: function(event) { |
| if (event.isPrimary) { |
| swipeTracker.pre_x = event.screenX; |
| swipeTracker.pre_y = event.screenY; |
| this.addEventListener("pointermove", this.swipeHandler, false); |
| } |
| }, |
| |
| /** |
| * Handles pointerup event. This is used for double tap/click events. |
| * @param {PointerEvent} event The pointerup event that bubbled to |
| * kb-keyboard. |
| */ |
| up: function(event) { |
| // When touch typing, it is very possible that finger moves slightly out |
| // of the key area before releases. The key should not be dropped in |
| // this case. |
| if (this.lastPressedKey && |
| this.lastPressedKey.pointerId == event.pointerId) { |
| this.lastPressedKey.autoRelease(); |
| } |
| |
| if (this.dblDetail_) { |
| this.dblDetail_.clickCount++; |
| if (this.dblDetail_.clickCount == 2) { |
| this.dblDetail_.callback(); |
| this.changeKeyset(this.dblDetail_); |
| clearTimeout(this.dblTimer_); |
| |
| this.classList.add('caps-locked'); |
| |
| this.dblDetail_ = null; |
| } |
| } |
| |
| // TODO(zyaozhujun): There are some edge cases to deal with later. |
| // (for instance, what if a second finger trigger a down and up |
| // event sequence while swiping). |
| // When pointer up from the screen, a swipe selection session finished, |
| // all the data should be reset to prepare for the next session. |
| if (event.isPrimary && swipeInProgress) { |
| swipeInProgress = false; |
| this.onSwipeUpdate(swipeTracker.populateDetails(true)) |
| swipeTracker.resetAll(); |
| } |
| this.removeEventListener('pointermove', this.swipeHandler, false); |
| }, |
| |
| /** |
| * Handles PointerOut event. This is used for when a swipe gesture goes |
| * outside of the keyboard window. |
| * @param {Object} event The pointerout event that bubbled to the |
| * kb-keyboard. |
| */ |
| out: function(event) { |
| // Ignore if triggered from one of the keys. |
| if (this.compareDocumentPosition(event.relatedTarget) & |
| Node.DOCUMENT_POSITION_CONTAINED_BY) |
| return; |
| if (swipeInProgress) |
| this.onSwipeUpdate(swipeTracker.populateDetails(true)) |
| // Touched outside of the keyboard area, so disables swipe. |
| swipeInProgress = false; |
| swipeTracker.resetAll(); |
| this.removeEventListener('pointermove', this.swipeHandler, false); |
| }, |
| |
| /** |
| * Handles key-up event that is sent by kb-key-base. |
| * @param {CustomEvent} event The key-up event dispatched by kb-key-base. |
| * @param {Object} detail The detail of pressed kb-key. |
| */ |
| keyUp: function(event, detail) { |
| if (this.skipEvent(detail)) |
| return; |
| if (swipeInProgress) |
| return; |
| if (detail.activeModifier) { |
| var modifier = detail.activeModifier.toLowerCase() + "-active"; |
| this.classList.add(modifier); |
| } |
| // Adds the current keyboard modifiers to the detail. |
| if (this.ctrl) |
| detail.controlModifier = this.ctrl.isActive(); |
| if (this.alt) |
| detail.altModifier = this.alt.isActive(); |
| if (this.lastPressedKey) |
| this.lastPressedKey.classList.remove('active'); |
| // Keyset transition key. This is needed to transition from upper |
| // to lower case when we are not in caps mode, as well as when |
| // we're ending chording. |
| this.changeKeyset(detail); |
| |
| if (this.lastPressedKey && |
| this.lastPressedKey.charValue != event.target.charValue) { |
| return; |
| } |
| if (repeatKey.key == event.target) { |
| repeatKey.cancel(); |
| this.lastPressedKey = null; |
| return; |
| } |
| var toLayoutId = detail.toLayout; |
| // Layout transition key. |
| if (toLayoutId) |
| this.layout = toLayoutId; |
| var char = detail.char; |
| this.lastPressedKey = null; |
| // Characters that should not be typed. |
| switch(char) { |
| case 'Invalid': |
| case 'Shift': |
| case 'Ctrl': |
| case 'Alt': |
| enterUpperOnSpace = false; |
| swipeTracker.swipeFlags = 0; |
| return; |
| case 'Microphone': |
| this.voiceInput_.onDown(); |
| return; |
| default: |
| break; |
| } |
| // Tries to type the character. Resorts to insertText if that fails. |
| if(!this.keyTyped(detail)) |
| insertText(char); |
| // Post-typing logic. |
| switch(char) { |
| case ' ': |
| if(enterUpperOnSpace) { |
| enterUpperOnSpace = false; |
| if (this.shift) { |
| var shiftDetail = this.shift.onSpaceAfterPunctuation(); |
| // Check if transition defined. |
| this.changeKeyset(shiftDetail); |
| } else { |
| console.error('Capitalization on space after punctuation \ |
| enabled, but cannot find target keyset.'); |
| } |
| // Immediately return to maintain shift-state. Space is a |
| // non-control key and would otherwise trigger a reset of the |
| // shift key, causing a transition to lower case. |
| // TODO(rsadam): Add unit test after Polymer uprev complete. |
| return; |
| } |
| break; |
| case '.': |
| case '?': |
| case '!': |
| enterUpperOnSpace = this.shouldUpperOnSpace(); |
| break; |
| default: |
| break; |
| } |
| // Reset control keys. |
| this.onNonControlKeyTyped(); |
| }, |
| |
| /* |
| * Handles key-longpress event that is sent by kb-key-base. |
| * @param {CustomEvent} event The key-longpress event dispatched by |
| * kb-key-base. |
| * @param {Object} detail The detail of pressed key. |
| */ |
| keyLongpress: function(event, detail) { |
| // If the gesture is long press, remove the pointermove listener. |
| this.removeEventListener('pointermove', this.swipeHandler, false); |
| // Keyset transtion key. |
| if (this.changeKeyset(detail)) { |
| // Locks the keyset before removing active to prevent flicker. |
| this.classList.add('caps-locked'); |
| // Makes last pressed key inactive if transit to a new keyset on long |
| // press. |
| if (this.lastPressedKey) |
| this.lastPressedKey.classList.remove('active'); |
| } |
| }, |
| |
| /** |
| * Whether we should transit to upper case when seeing a space after |
| * punctuation. |
| * @return {boolean} |
| */ |
| shouldUpperOnSpace: function() { |
| // TODO(rsadam): Add other input types in which we should not |
| // transition to upper after a space. |
| return this.inputTypeValue != 'password'; |
| }, |
| |
| /** |
| * Show menu for selecting a keyboard layout. |
| * @param {!Event} event The triggering event. |
| * @param {{left: number, top: number, width: number}} details Location of |
| * the button that triggered the popup. |
| */ |
| showOptions: function(event, details) { |
| var overlay = this.$.overlay; |
| if (!overlay) { |
| console.error('Missing overlay.'); |
| return; |
| } |
| var menu = overlay.$.options; |
| if (!menu) { |
| console.error('Missing options menu.'); |
| } |
| |
| menu.hidden = false; |
| overlay.hidden = false; |
| var left = details.left + details.width - menu.clientWidth; |
| var top = details.top - menu.clientHeight; |
| menu.style.left = left + 'px'; |
| menu.style.top = top + 'px'; |
| }, |
| |
| /** |
| * Handler for the 'set-layout' event. |
| * @param {!Event} event The triggering event. |
| * @param {{layout: string}} details Details of the event, which contains |
| * the name of the layout to activate. |
| */ |
| setLayout: function(event, details) { |
| this.layout = details.layout; |
| }, |
| |
| /** |
| * Handles a change in the keyboard layout. Auto-selects the default |
| * keyset for the new layout. |
| */ |
| layoutChanged: function() { |
| if (!this.selectDefaultKeyset()) { |
| this.fire('stateChange', {state: 'loadingKeyset'}); |
| |
| // Keyset selection fails if the keysets have not been loaded yet. |
| var keysets = document.querySelector('#' + this.layout); |
| if (keysets && keysets.content) { |
| var content = flattenKeysets(keysets.content); |
| this.appendChild(content); |
| this.selectDefaultKeyset(); |
| } else { |
| // Add link for the keysets if missing from the document. Force |
| // a layout change after resolving the import of the link. |
| var query = 'link[id=' + this.layout + ']'; |
| if (!document.querySelector(query)) { |
| // Layout has not beeen loaded yet. |
| var link = document.createElement('link'); |
| link.id = this.layout; |
| link.setAttribute('rel', 'import'); |
| link.setAttribute('href', 'layouts/' + this.layout + '.html'); |
| document.head.appendChild(link); |
| |
| // Load content for the new link element. |
| var self = this; |
| HTMLImports.importer.load(document, function() { |
| HTMLImports.parser.parseLink(link); |
| self.layoutChanged(); |
| }); |
| } |
| } |
| } |
| }, |
| |
| /** |
| * Notifies the modifier keys that a non-control key was typed. This |
| * lets them reset sticky behaviour. A non-control key is defined as |
| * any key that is not Control, Alt, or Shift. |
| */ |
| onNonControlKeyTyped: function() { |
| if (this.shift) |
| this.shift.onNonControlKeyTyped(); |
| if (this.ctrl) |
| this.ctrl.onNonControlKeyTyped(); |
| if (this.alt) |
| this.alt.onNonControlKeyTyped(); |
| this.classList.remove('ctrl-active'); |
| this.classList.remove('alt-active'); |
| }, |
| |
| /** |
| * Id for the active keyset. |
| * @type {string} |
| */ |
| get activeKeysetId() { |
| return this.layout + '-' + this.keyset; |
| }, |
| |
| /** |
| * The active keyset DOM object. |
| * @type {kb-keyset} |
| */ |
| get activeKeyset() { |
| return this.querySelector('#' + this.activeKeysetId); |
| }, |
| |
| /** |
| * The current input type. |
| * @type {string} |
| */ |
| get inputTypeValue() { |
| return this.inputType; |
| }, |
| |
| /** |
| * Changes the input type if it's different from the current |
| * type, else resets the keyset to the default keyset. |
| * @type {string} |
| */ |
| set inputTypeValue(value) { |
| if (value == this.inputType) |
| this.selectDefaultKeyset(); |
| else |
| this.inputType = value; |
| }, |
| |
| /** |
| * The keyboard is ready for input once the target keyset appears |
| * in the distributed nodes for the keyboard. |
| * @return {boolean} Indicates if the keyboard is ready for input. |
| */ |
| isReady: function() { |
| var keyset = this.activeKeyset; |
| if (!keyset) |
| return false; |
| var content = this.$.content.getDistributedNodes()[0]; |
| return content == keyset; |
| }, |
| |
| /** |
| * Generates fabricated key events to simulate typing on a |
| * physical keyboard. |
| * @param {Object} detail Attributes of the key being typed. |
| * @return {boolean} Whether the key type succeeded. |
| */ |
| keyTyped: function(detail) { |
| var builder = this.$.keyCodeMetadata; |
| if (this.shift) |
| detail.shiftModifier = this.shift.isActive(); |
| if (this.ctrl) |
| detail.controlModifier = this.ctrl.isActive(); |
| if (this.alt) |
| detail.altModifier = this.alt.isActive(); |
| var downEvent = builder.createVirtualKeyEvent(detail, "keydown"); |
| if (downEvent) { |
| sendKeyEvent(downEvent); |
| sendKeyEvent(builder.createVirtualKeyEvent(detail, "keyup")); |
| return true; |
| } |
| return false; |
| }, |
| |
| /** |
| * Selects the default keyset for a layout. |
| * @return {boolean} True if successful. This method can fail if the |
| * keysets corresponding to the layout have not been injected. |
| */ |
| selectDefaultKeyset: function() { |
| var keysets = this.querySelectorAll('kb-keyset'); |
| // Full name of the keyset is of the form 'layout-keyset'. |
| var regex = new RegExp('^' + this.layout + '-(.+)'); |
| var keysetsLoaded = false; |
| for (var i = 0; i < keysets.length; i++) { |
| var matches = keysets[i].id.match(regex); |
| if (matches && matches.length == REGEX_MATCH_COUNT) { |
| keysetsLoaded = true; |
| // Without both tests for a default keyset, it is possible to get |
| // into a state where multiple layouts are displayed. A |
| // reproducable test case is do the following set of keyset |
| // transitions: qwerty -> system -> dvorak -> qwerty. |
| // TODO(kevers): Investigate why this is the case. |
| if (keysets[i].isDefault || |
| keysets[i].getAttribute('isDefault') == 'true') { |
| this.keyset = matches[REGEX_KEYSET_INDEX]; |
| this.classList.remove('caps-locked'); |
| this.classList.remove('alt-active'); |
| this.classList.remove('ctrl-active'); |
| // Caches shift key. |
| this.shift = this.querySelector('kb-shift-key'); |
| if (this.shift) |
| this.shift.reset(); |
| // Caches control key. |
| this.ctrl = this.querySelector('kb-modifier-key[char=Ctrl]'); |
| if (this.ctrl) |
| this.ctrl.reset(); |
| // Caches alt key. |
| this.alt = this.querySelector('kb-modifier-key[char=Alt]'); |
| if (this.alt) |
| this.alt.reset(); |
| this.fire('stateChange', { |
| state: 'keysetLoaded', |
| value: this.keyset, |
| }); |
| keyboardLoaded(); |
| return true; |
| } |
| } |
| } |
| if (keysetsLoaded) |
| console.error('No default keyset found for ' + this.layout); |
| return false; |
| } |
| }); |
| </script> |
| </polymer-element> |