| <!-- |
| @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+0008': 'backspace', |
| 'U+0009': 'tab', |
| 'U+001B': 'esc', |
| 'U+0020': 'space', |
| '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 = { |
| 8: 'backspace', |
| 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)?/; |
| |
| /** |
| * Matches ESC key. |
| * |
| * Value from: http://w3c.github.io/uievents-key/#key-Escape |
| */ |
| var ESC_KEY = /^escape$/; |
| |
| /** |
| * Transforms the key. |
| * @param {string} key The KeyBoardEvent.key |
| * @param {Boolean} [noSpecialChars] Limits the transformation to |
| * alpha-numeric characters. |
| */ |
| function transformKey(key, noSpecialChars) { |
| var validKey = ''; |
| if (key) { |
| var lKey = key.toLowerCase(); |
| if (lKey === ' ' || SPACE_KEY.test(lKey)) { |
| validKey = 'space'; |
| } else if (ESC_KEY.test(lKey)) { |
| validKey = 'esc'; |
| } else if (lKey.length == 1) { |
| if (!noSpecialChars || KEY_CHAR.test(lKey)) { |
| validKey = lKey; |
| } |
| } else if (ARROW_KEY.test(lKey)) { |
| validKey = lKey.replace('arrow', ''); |
| } 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 (keyIdent in KEY_IDENTIFIER) { |
| validKey = KEY_IDENTIFIER[keyIdent]; |
| } else if (IDENT_CHAR.test(keyIdent)) { |
| keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); |
| validKey = String.fromCharCode(keyIdent).toLowerCase(); |
| } 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(keyCode - 48); |
| } else if (keyCode >= 96 && keyCode <= 105) { |
| // num pad 0-9 |
| validKey = String(keyCode - 96); |
| } else { |
| validKey = KEY_CODE[keyCode]; |
| } |
| } |
| return validKey; |
| } |
| |
| /** |
| * Calculates the normalized key for a KeyboardEvent. |
| * @param {KeyboardEvent} keyEvent |
| * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key |
| * transformation to alpha-numeric chars. This is useful with key |
| * combinations like shift + 2, which on FF for MacOS produces |
| * keyEvent.key = @ |
| * To get 2 returned, set noSpecialChars = true |
| * To get @ returned, set noSpecialChars = false |
| */ |
| function normalizedKeyForEvent(keyEvent, noSpecialChars) { |
| // Fall back from .key, to .keyIdentifier, to .keyCode, and then to |
| // .detail.key to support artificial keyboard events. |
| return transformKey(keyEvent.key, noSpecialChars) || |
| transformKeyIdentifier(keyEvent.keyIdentifier) || |
| transformKeyCode(keyEvent.keyCode) || |
| transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || ''; |
| } |
| |
| function keyComboMatchesEvent(keyCombo, event) { |
| // For combos with modifiers we support only alpha-numeric keys |
| var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); |
| return keyEvent === keyCombo.key && |
| (!keyCombo.hasModifiers || ( |
| !!event.shiftKey === !!keyCombo.shiftKey && |
| !!event.ctrlKey === !!keyCombo.ctrlKey && |
| !!event.altKey === !!keyCombo.altKey && |
| !!event.metaKey === !!keyCombo.metaKey) |
| ); |
| } |
| |
| function parseKeyComboString(keyComboString) { |
| if (keyComboString.length === 1) { |
| return { |
| combo: keyComboString, |
| key: keyComboString, |
| event: 'keydown' |
| }; |
| } |
| 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; |
| parsedKeyCombo.hasModifiers = true; |
| } else { |
| parsedKeyCombo.key = keyName; |
| parsedKeyCombo.event = event || 'keydown'; |
| } |
| |
| return parsedKeyCombo; |
| }, { |
| combo: keyComboString.split(':').shift() |
| }); |
| } |
| |
| function parseEventString(eventString) { |
| return eventString.trim().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 callback. A key binding has the format |
| * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or |
| * `"KEY:EVENT": "callback"` are valid as well). Some examples: |
| * |
| * keyBindings: { |
| * 'space': '_onKeydown', // same as 'space:keydown' |
| * 'shift+tab': '_onKeydown', |
| * 'enter:keypress': '_onKeypress', |
| * 'esc:keyup': '_onKeyup' |
| * } |
| * |
| * The callback will receive with an event containing the following information in `event.detail`: |
| * |
| * _onKeydown: function(event) { |
| * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab" |
| * console.log(event.detail.key); // KEY only, e.g. "tab" |
| * console.log(event.detail.event); // EVENT, e.g. "keydown" |
| * console.log(event.detail.keyboardEvent); // the original KeyboardEvent |
| * } |
| * |
| * Use the `keyEventTarget` attribute to set up event handlers on a specific |
| * node. |
| * |
| * See the [demo source code](https://github.com/PolymerElements/iron-a11y-keys-behavior/blob/master/demo/x-key-aware.html) |
| * for an example. |
| * |
| * @demo demo/index.html |
| * @polymerBehavior |
| */ |
| Polymer.IronA11yKeysBehavior = { |
| properties: { |
| /** |
| * The EventTarget that will be firing relevant KeyboardEvents. Set it to |
| * `null` to disable the listeners. |
| * @type {?EventTarget} |
| */ |
| 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)' |
| ], |
| |
| |
| /** |
| * To be used to express what combination of keys will trigger the relative |
| * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}` |
| * @type {Object} |
| */ |
| 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(); |
| }, |
| |
| /** |
| * Returns true if a keyboard event matches `eventString`. |
| * |
| * @param {KeyboardEvent} event |
| * @param {string} eventString |
| * @return {boolean} |
| */ |
| keyboardEventMatchesKeys: function(event, eventString) { |
| var keyCombos = parseEventString(eventString); |
| for (var i = 0; i < keyCombos.length; ++i) { |
| if (keyComboMatchesEvent(keyCombos[i], 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]); |
| } |
| |
| // Give precedence to combos with modifiers to be checked first. |
| for (var eventName in this._keyBindings) { |
| this._keyBindings[eventName].sort(function (kb1, kb2) { |
| var b1 = kb1[0].hasModifiers; |
| var b2 = kb2[0].hasModifiers; |
| return (b1 === b2) ? 0 : b1 ? -1 : 1; |
| }) |
| } |
| }, |
| |
| _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() { |
| if (!this.keyEventTarget) { |
| return; |
| } |
| 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(); |
| } |
| |
| // if event has been already prevented, don't do anything |
| if (event.defaultPrevented) { |
| return; |
| } |
| |
| for (var i = 0; i < keyBindings.length; i++) { |
| var keyCombo = keyBindings[i][0]; |
| var handlerName = keyBindings[i][1]; |
| if (keyComboMatchesEvent(keyCombo, event)) { |
| this._triggerKeyHandler(keyCombo, handlerName, event); |
| // exit the loop if eventDefault was prevented |
| if (event.defaultPrevented) { |
| return; |
| } |
| } |
| } |
| }, |
| |
| _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> |