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 </script> 979</polymer-element> 980