UNPKG

spincycle

Version:

A reactive message router and object manager that lets clients subscribe to object property changes on the server

497 lines (443 loc) 15.8 kB
<!-- @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 .detail.key for artifical keyboard events, // and then to deprecated .keyIdentifier and .keyCode. if (keyEvent.key) { return transformKey(keyEvent.key, noSpecialChars); } if (keyEvent.detail && keyEvent.detail.key) { return transformKey(keyEvent.detail.key, noSpecialChars); } return transformKeyIdentifier(keyEvent.keyIdentifier) || transformKeyCode(keyEvent.keyCode) || ''; } 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>