Html程序  |  980行  |  31.63 KB

<!--
  -- 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}}" on-type-key="{{type}}"
    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
    };

    /**
     * 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) || (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;
          if (triggerKey && triggerKey.onFlick)
            triggerKey.onFlick(detail);
        }
      },

      /**
       * 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 a TypeKey event. This is used for when we programmatically
       * want to type a specific key.
       * @param {CustomEvent} event The TypeKey event that bubbled to the
       *    kb-keyboard.
       */
      type: function(event) {
        this.keyTyped(event.detail);
      },

      /**
       * 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>