<!-- @license Copyright (c) 2015 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt --> <link rel="import" href="../polymer/polymer.html"> <script> (function() { 'use strict'; /** * Chrome uses an older version of DOM Level 3 Keyboard Events * * Most keys are labeled as text, but some are Unicode codepoints. * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set */ var KEY_IDENTIFIER = { 'U+0009': 'tab', 'U+001B': 'esc', 'U+0020': 'space', 'U+002A': '*', 'U+0030': '0', 'U+0031': '1', 'U+0032': '2', 'U+0033': '3', 'U+0034': '4', 'U+0035': '5', 'U+0036': '6', 'U+0037': '7', 'U+0038': '8', 'U+0039': '9', 'U+0041': 'a', 'U+0042': 'b', 'U+0043': 'c', 'U+0044': 'd', 'U+0045': 'e', 'U+0046': 'f', 'U+0047': 'g', 'U+0048': 'h', 'U+0049': 'i', 'U+004A': 'j', 'U+004B': 'k', 'U+004C': 'l', 'U+004D': 'm', 'U+004E': 'n', 'U+004F': 'o', 'U+0050': 'p', 'U+0051': 'q', 'U+0052': 'r', 'U+0053': 's', 'U+0054': 't', 'U+0055': 'u', 'U+0056': 'v', 'U+0057': 'w', 'U+0058': 'x', 'U+0059': 'y', 'U+005A': 'z', 'U+007F': 'del' }; /** * Special table for KeyboardEvent.keyCode. * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better * than that. * * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode */ var KEY_CODE = { 9: 'tab', 13: 'enter', 27: 'esc', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 32: 'space', 37: 'left', 38: 'up', 39: 'right', 40: 'down', 46: 'del', 106: '*' }; /** * MODIFIER_KEYS maps the short name for modifier keys used in a key * combo string to the property name that references those same keys * in a KeyboardEvent instance. */ var MODIFIER_KEYS = { 'shift': 'shiftKey', 'ctrl': 'ctrlKey', 'alt': 'altKey', 'meta': 'metaKey' }; /** * KeyboardEvent.key is mostly represented by printable character made by * the keyboard, with unprintable keys labeled nicely. * * However, on OS X, Alt+char can make a Unicode character that follows an * Apple-specific mapping. In this case, we * fall back to .keyCode. */ var KEY_CHAR = /[a-z0-9*]/; /** * Matches a keyIdentifier string. */ var IDENT_CHAR = /U\+/; /** * Matches arrow keys in Gecko 27.0+ */ var ARROW_KEY = /^arrow/; /** * Matches space keys everywhere (notably including IE10's exceptional name * `spacebar`). */ var SPACE_KEY = /^space(bar)?/; function transformKey(key) { var validKey = ''; if (key) { var lKey = key.toLowerCase(); if (lKey.length == 1) { if (KEY_CHAR.test(lKey)) { validKey = lKey; } } else if (ARROW_KEY.test(lKey)) { validKey = lKey.replace('arrow', ''); } else if (SPACE_KEY.test(lKey)) { validKey = 'space'; } else if (lKey == 'multiply') { // numpad '*' can map to Multiply on IE/Windows validKey = '*'; } else { validKey = lKey; } } return validKey; } function transformKeyIdentifier(keyIdent) { var validKey = ''; if (keyIdent) { if (IDENT_CHAR.test(keyIdent)) { validKey = KEY_IDENTIFIER[keyIdent]; } else { validKey = keyIdent.toLowerCase(); } } return validKey; } function transformKeyCode(keyCode) { var validKey = ''; if (Number(keyCode)) { if (keyCode >= 65 && keyCode <= 90) { // ascii a-z // lowercase is 32 offset from uppercase validKey = String.fromCharCode(32 + keyCode); } else if (keyCode >= 112 && keyCode <= 123) { // function keys f1-f12 validKey = 'f' + (keyCode - 112); } else if (keyCode >= 48 && keyCode <= 57) { // top 0-9 keys validKey = String(48 - keyCode); } else if (keyCode >= 96 && keyCode <= 105) { // num pad 0-9 validKey = String(96 - keyCode); } else { validKey = KEY_CODE[keyCode]; } } return validKey; } function normalizedKeyForEvent(keyEvent) { // fall back from .key, to .keyIdentifier, to .keyCode, and then to // .detail.key to support artificial keyboard events return transformKey(keyEvent.key) || transformKeyIdentifier(keyEvent.keyIdentifier) || transformKeyCode(keyEvent.keyCode) || transformKey(keyEvent.detail.key) || ''; } function keyComboMatchesEvent(keyCombo, keyEvent) { return normalizedKeyForEvent(keyEvent) === keyCombo.key && !!keyEvent.shiftKey === !!keyCombo.shiftKey && !!keyEvent.ctrlKey === !!keyCombo.ctrlKey && !!keyEvent.altKey === !!keyCombo.altKey && !!keyEvent.metaKey === !!keyCombo.metaKey; } function parseKeyComboString(keyComboString) { return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) { var eventParts = keyComboPart.split(':'); var keyName = eventParts[0]; var event = eventParts[1]; if (keyName in MODIFIER_KEYS) { parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; } else { parsedKeyCombo.key = keyName; parsedKeyCombo.event = event || 'keydown'; } return parsedKeyCombo; }, { combo: keyComboString.split(':').shift() }); } function parseEventString(eventString) { return eventString.split(' ').map(function(keyComboString) { return parseKeyComboString(keyComboString); }); } /** * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding). * The element takes care of browser differences with respect to Keyboard events * and uses an expressive syntax to filter key presses. * * Use the `keyBindings` prototype property to express what combination of keys * will trigger the event to fire. * * Use the `key-event-target` attribute to set up event handlers on a specific * node. * The `keys-pressed` event will fire when one of the key combinations set with the * `keys` property is pressed. * * @demo demo/index.html * @polymerBehavior */ Polymer.IronA11yKeysBehavior = { properties: { /** * The HTMLElement that will be firing relevant KeyboardEvents. */ keyEventTarget: { type: Object, value: function() { return this; } }, /** * If true, this property will cause the implementing element to * automatically stop propagation on any handled KeyboardEvents. */ stopKeyboardEventPropagation: { type: Boolean, value: false }, _boundKeyHandlers: { type: Array, value: function() { return []; } }, // We use this due to a limitation in IE10 where instances will have // own properties of everything on the "prototype". _imperativeKeyBindings: { type: Object, value: function() { return {}; } } }, observers: [ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' ], keyBindings: {}, registered: function() { this._prepKeyBindings(); }, attached: function() { this._listenKeyEventListeners(); }, detached: function() { this._unlistenKeyEventListeners(); }, /** * Can be used to imperatively add a key binding to the implementing * element. This is the imperative equivalent of declaring a keybinding * in the `keyBindings` prototype property. */ addOwnKeyBinding: function(eventString, handlerName) { this._imperativeKeyBindings[eventString] = handlerName; this._prepKeyBindings(); this._resetKeyEventListeners(); }, /** * When called, will remove all imperatively-added key bindings. */ removeOwnKeyBindings: function() { this._imperativeKeyBindings = {}; this._prepKeyBindings(); this._resetKeyEventListeners(); }, keyboardEventMatchesKeys: function(event, eventString) { var keyCombos = parseEventString(eventString); var index; for (index = 0; index < keyCombos.length; ++index) { if (keyComboMatchesEvent(keyCombos[index], event)) { return true; } } return false; }, _collectKeyBindings: function() { var keyBindings = this.behaviors.map(function(behavior) { return behavior.keyBindings; }); if (keyBindings.indexOf(this.keyBindings) === -1) { keyBindings.push(this.keyBindings); } return keyBindings; }, _prepKeyBindings: function() { this._keyBindings = {}; this._collectKeyBindings().forEach(function(keyBindings) { for (var eventString in keyBindings) { this._addKeyBinding(eventString, keyBindings[eventString]); } }, this); for (var eventString in this._imperativeKeyBindings) { this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]); } }, _addKeyBinding: function(eventString, handlerName) { parseEventString(eventString).forEach(function(keyCombo) { this._keyBindings[keyCombo.event] = this._keyBindings[keyCombo.event] || []; this._keyBindings[keyCombo.event].push([ keyCombo, handlerName ]); }, this); }, _resetKeyEventListeners: function() { this._unlistenKeyEventListeners(); if (this.isAttached) { this._listenKeyEventListeners(); } }, _listenKeyEventListeners: function() { Object.keys(this._keyBindings).forEach(function(eventName) { var keyBindings = this._keyBindings[eventName]; var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]); this.keyEventTarget.addEventListener(eventName, boundKeyHandler); }, this); }, _unlistenKeyEventListeners: function() { var keyHandlerTuple; var keyEventTarget; var eventName; var boundKeyHandler; while (this._boundKeyHandlers.length) { // My kingdom for block-scope binding and destructuring assignment.. keyHandlerTuple = this._boundKeyHandlers.pop(); keyEventTarget = keyHandlerTuple[0]; eventName = keyHandlerTuple[1]; boundKeyHandler = keyHandlerTuple[2]; keyEventTarget.removeEventListener(eventName, boundKeyHandler); } }, _onKeyBindingEvent: function(keyBindings, event) { if (this.stopKeyboardEventPropagation) { event.stopPropagation(); } keyBindings.forEach(function(keyBinding) { var keyCombo = keyBinding[0]; var handlerName = keyBinding[1]; if (!event.defaultPrevented && keyComboMatchesEvent(keyCombo, event)) { this._triggerKeyHandler(keyCombo, handlerName, event); } }, this); }, _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { var detail = Object.create(keyCombo); detail.keyboardEvent = keyboardEvent; var event = new CustomEvent(keyCombo.event, { detail: detail, cancelable: true }); this[handlerName].call(this, event); if (event.defaultPrevented) { keyboardEvent.preventDefault(); } } }; })(); </script>