• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!--
2@license
3Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
9-->
10
11<link rel="import" href="../polymer/polymer.html">
12
13<script>
14  (function() {
15    'use strict';
16
17    /**
18     * Chrome uses an older version of DOM Level 3 Keyboard Events
19     *
20     * Most keys are labeled as text, but some are Unicode codepoints.
21     * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
22     */
23    var KEY_IDENTIFIER = {
24      'U+0008': 'backspace',
25      'U+0009': 'tab',
26      'U+001B': 'esc',
27      'U+0020': 'space',
28      'U+007F': 'del'
29    };
30
31    /**
32     * Special table for KeyboardEvent.keyCode.
33     * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better
34     * than that.
35     *
36     * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
37     */
38    var KEY_CODE = {
39      8: 'backspace',
40      9: 'tab',
41      13: 'enter',
42      27: 'esc',
43      33: 'pageup',
44      34: 'pagedown',
45      35: 'end',
46      36: 'home',
47      32: 'space',
48      37: 'left',
49      38: 'up',
50      39: 'right',
51      40: 'down',
52      46: 'del',
53      106: '*'
54    };
55
56    /**
57     * MODIFIER_KEYS maps the short name for modifier keys used in a key
58     * combo string to the property name that references those same keys
59     * in a KeyboardEvent instance.
60     */
61    var MODIFIER_KEYS = {
62      'shift': 'shiftKey',
63      'ctrl': 'ctrlKey',
64      'alt': 'altKey',
65      'meta': 'metaKey'
66    };
67
68    /**
69     * KeyboardEvent.key is mostly represented by printable character made by
70     * the keyboard, with unprintable keys labeled nicely.
71     *
72     * However, on OS X, Alt+char can make a Unicode character that follows an
73     * Apple-specific mapping. In this case, we fall back to .keyCode.
74     */
75    var KEY_CHAR = /[a-z0-9*]/;
76
77    /**
78     * Matches a keyIdentifier string.
79     */
80    var IDENT_CHAR = /U\+/;
81
82    /**
83     * Matches arrow keys in Gecko 27.0+
84     */
85    var ARROW_KEY = /^arrow/;
86
87    /**
88     * Matches space keys everywhere (notably including IE10's exceptional name
89     * `spacebar`).
90     */
91    var SPACE_KEY = /^space(bar)?/;
92
93    /**
94     * Matches ESC key.
95     *
96     * Value from: http://w3c.github.io/uievents-key/#key-Escape
97     */
98    var ESC_KEY = /^escape$/;
99
100    /**
101     * Transforms the key.
102     * @param {string} key The KeyBoardEvent.key
103     * @param {Boolean} [noSpecialChars] Limits the transformation to
104     * alpha-numeric characters.
105     */
106    function transformKey(key, noSpecialChars) {
107      var validKey = '';
108      if (key) {
109        var lKey = key.toLowerCase();
110        if (lKey === ' ' || SPACE_KEY.test(lKey)) {
111          validKey = 'space';
112        } else if (ESC_KEY.test(lKey)) {
113          validKey = 'esc';
114        } else if (lKey.length == 1) {
115          if (!noSpecialChars || KEY_CHAR.test(lKey)) {
116            validKey = lKey;
117          }
118        } else if (ARROW_KEY.test(lKey)) {
119          validKey = lKey.replace('arrow', '');
120        } else if (lKey == 'multiply') {
121          // numpad '*' can map to Multiply on IE/Windows
122          validKey = '*';
123        } else {
124          validKey = lKey;
125        }
126      }
127      return validKey;
128    }
129
130    function transformKeyIdentifier(keyIdent) {
131      var validKey = '';
132      if (keyIdent) {
133        if (keyIdent in KEY_IDENTIFIER) {
134          validKey = KEY_IDENTIFIER[keyIdent];
135        } else if (IDENT_CHAR.test(keyIdent)) {
136          keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
137          validKey = String.fromCharCode(keyIdent).toLowerCase();
138        } else {
139          validKey = keyIdent.toLowerCase();
140        }
141      }
142      return validKey;
143    }
144
145    function transformKeyCode(keyCode) {
146      var validKey = '';
147      if (Number(keyCode)) {
148        if (keyCode >= 65 && keyCode <= 90) {
149          // ascii a-z
150          // lowercase is 32 offset from uppercase
151          validKey = String.fromCharCode(32 + keyCode);
152        } else if (keyCode >= 112 && keyCode <= 123) {
153          // function keys f1-f12
154          validKey = 'f' + (keyCode - 112);
155        } else if (keyCode >= 48 && keyCode <= 57) {
156          // top 0-9 keys
157          validKey = String(keyCode - 48);
158        } else if (keyCode >= 96 && keyCode <= 105) {
159          // num pad 0-9
160          validKey = String(keyCode - 96);
161        } else {
162          validKey = KEY_CODE[keyCode];
163        }
164      }
165      return validKey;
166    }
167
168    /**
169      * Calculates the normalized key for a KeyboardEvent.
170      * @param {KeyboardEvent} keyEvent
171      * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
172      * transformation to alpha-numeric chars. This is useful with key
173      * combinations like shift + 2, which on FF for MacOS produces
174      * keyEvent.key = @
175      * To get 2 returned, set noSpecialChars = true
176      * To get @ returned, set noSpecialChars = false
177     */
178    function normalizedKeyForEvent(keyEvent, noSpecialChars) {
179      // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
180      // .detail.key to support artificial keyboard events.
181      return transformKey(keyEvent.key, noSpecialChars) ||
182        transformKeyIdentifier(keyEvent.keyIdentifier) ||
183        transformKeyCode(keyEvent.keyCode) ||
184        transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || '';
185    }
186
187    function keyComboMatchesEvent(keyCombo, event) {
188      // For combos with modifiers we support only alpha-numeric keys
189      var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
190      return keyEvent === keyCombo.key &&
191        (!keyCombo.hasModifiers || (
192          !!event.shiftKey === !!keyCombo.shiftKey &&
193          !!event.ctrlKey === !!keyCombo.ctrlKey &&
194          !!event.altKey === !!keyCombo.altKey &&
195          !!event.metaKey === !!keyCombo.metaKey)
196        );
197    }
198
199    function parseKeyComboString(keyComboString) {
200      if (keyComboString.length === 1) {
201        return {
202          combo: keyComboString,
203          key: keyComboString,
204          event: 'keydown'
205        };
206      }
207      return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) {
208        var eventParts = keyComboPart.split(':');
209        var keyName = eventParts[0];
210        var event = eventParts[1];
211
212        if (keyName in MODIFIER_KEYS) {
213          parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
214          parsedKeyCombo.hasModifiers = true;
215        } else {
216          parsedKeyCombo.key = keyName;
217          parsedKeyCombo.event = event || 'keydown';
218        }
219
220        return parsedKeyCombo;
221      }, {
222        combo: keyComboString.split(':').shift()
223      });
224    }
225
226    function parseEventString(eventString) {
227      return eventString.trim().split(' ').map(function(keyComboString) {
228        return parseKeyComboString(keyComboString);
229      });
230    }
231
232    /**
233     * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
234     * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
235     * The element takes care of browser differences with respect to Keyboard events
236     * and uses an expressive syntax to filter key presses.
237     *
238     * Use the `keyBindings` prototype property to express what combination of keys
239     * will trigger the callback. A key binding has the format
240     * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
241     * `"KEY:EVENT": "callback"` are valid as well). Some examples:
242     *
243     *      keyBindings: {
244     *        'space': '_onKeydown', // same as 'space:keydown'
245     *        'shift+tab': '_onKeydown',
246     *        'enter:keypress': '_onKeypress',
247     *        'esc:keyup': '_onKeyup'
248     *      }
249     *
250     * The callback will receive with an event containing the following information in `event.detail`:
251     *
252     *      _onKeydown: function(event) {
253     *        console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
254     *        console.log(event.detail.key); // KEY only, e.g. "tab"
255     *        console.log(event.detail.event); // EVENT, e.g. "keydown"
256     *        console.log(event.detail.keyboardEvent); // the original KeyboardEvent
257     *      }
258     *
259     * Use the `keyEventTarget` attribute to set up event handlers on a specific
260     * node.
261     *
262     * See the [demo source code](https://github.com/PolymerElements/iron-a11y-keys-behavior/blob/master/demo/x-key-aware.html)
263     * for an example.
264     *
265     * @demo demo/index.html
266     * @polymerBehavior
267     */
268    Polymer.IronA11yKeysBehavior = {
269      properties: {
270        /**
271         * The EventTarget that will be firing relevant KeyboardEvents. Set it to
272         * `null` to disable the listeners.
273         * @type {?EventTarget}
274         */
275        keyEventTarget: {
276          type: Object,
277          value: function() {
278            return this;
279          }
280        },
281
282        /**
283         * If true, this property will cause the implementing element to
284         * automatically stop propagation on any handled KeyboardEvents.
285         */
286        stopKeyboardEventPropagation: {
287          type: Boolean,
288          value: false
289        },
290
291        _boundKeyHandlers: {
292          type: Array,
293          value: function() {
294            return [];
295          }
296        },
297
298        // We use this due to a limitation in IE10 where instances will have
299        // own properties of everything on the "prototype".
300        _imperativeKeyBindings: {
301          type: Object,
302          value: function() {
303            return {};
304          }
305        }
306      },
307
308      observers: [
309        '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
310      ],
311
312
313      /**
314       * To be used to express what combination of keys  will trigger the relative
315       * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
316       * @type {Object}
317       */
318      keyBindings: {},
319
320      registered: function() {
321        this._prepKeyBindings();
322      },
323
324      attached: function() {
325        this._listenKeyEventListeners();
326      },
327
328      detached: function() {
329        this._unlistenKeyEventListeners();
330      },
331
332      /**
333       * Can be used to imperatively add a key binding to the implementing
334       * element. This is the imperative equivalent of declaring a keybinding
335       * in the `keyBindings` prototype property.
336       */
337      addOwnKeyBinding: function(eventString, handlerName) {
338        this._imperativeKeyBindings[eventString] = handlerName;
339        this._prepKeyBindings();
340        this._resetKeyEventListeners();
341      },
342
343      /**
344       * When called, will remove all imperatively-added key bindings.
345       */
346      removeOwnKeyBindings: function() {
347        this._imperativeKeyBindings = {};
348        this._prepKeyBindings();
349        this._resetKeyEventListeners();
350      },
351
352      /**
353       * Returns true if a keyboard event matches `eventString`.
354       *
355       * @param {KeyboardEvent} event
356       * @param {string} eventString
357       * @return {boolean}
358       */
359      keyboardEventMatchesKeys: function(event, eventString) {
360        var keyCombos = parseEventString(eventString);
361        for (var i = 0; i < keyCombos.length; ++i) {
362          if (keyComboMatchesEvent(keyCombos[i], event)) {
363            return true;
364          }
365        }
366        return false;
367      },
368
369      _collectKeyBindings: function() {
370        var keyBindings = this.behaviors.map(function(behavior) {
371          return behavior.keyBindings;
372        });
373
374        if (keyBindings.indexOf(this.keyBindings) === -1) {
375          keyBindings.push(this.keyBindings);
376        }
377
378        return keyBindings;
379      },
380
381      _prepKeyBindings: function() {
382        this._keyBindings = {};
383
384        this._collectKeyBindings().forEach(function(keyBindings) {
385          for (var eventString in keyBindings) {
386            this._addKeyBinding(eventString, keyBindings[eventString]);
387          }
388        }, this);
389
390        for (var eventString in this._imperativeKeyBindings) {
391          this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]);
392        }
393
394        // Give precedence to combos with modifiers to be checked first.
395        for (var eventName in this._keyBindings) {
396          this._keyBindings[eventName].sort(function (kb1, kb2) {
397            var b1 = kb1[0].hasModifiers;
398            var b2 = kb2[0].hasModifiers;
399            return (b1 === b2) ? 0 : b1 ? -1 : 1;
400          })
401        }
402      },
403
404      _addKeyBinding: function(eventString, handlerName) {
405        parseEventString(eventString).forEach(function(keyCombo) {
406          this._keyBindings[keyCombo.event] =
407            this._keyBindings[keyCombo.event] || [];
408
409          this._keyBindings[keyCombo.event].push([
410            keyCombo,
411            handlerName
412          ]);
413        }, this);
414      },
415
416      _resetKeyEventListeners: function() {
417        this._unlistenKeyEventListeners();
418
419        if (this.isAttached) {
420          this._listenKeyEventListeners();
421        }
422      },
423
424      _listenKeyEventListeners: function() {
425        if (!this.keyEventTarget) {
426          return;
427        }
428        Object.keys(this._keyBindings).forEach(function(eventName) {
429          var keyBindings = this._keyBindings[eventName];
430          var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
431
432          this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
433
434          this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
435        }, this);
436      },
437
438      _unlistenKeyEventListeners: function() {
439        var keyHandlerTuple;
440        var keyEventTarget;
441        var eventName;
442        var boundKeyHandler;
443
444        while (this._boundKeyHandlers.length) {
445          // My kingdom for block-scope binding and destructuring assignment..
446          keyHandlerTuple = this._boundKeyHandlers.pop();
447          keyEventTarget = keyHandlerTuple[0];
448          eventName = keyHandlerTuple[1];
449          boundKeyHandler = keyHandlerTuple[2];
450
451          keyEventTarget.removeEventListener(eventName, boundKeyHandler);
452        }
453      },
454
455      _onKeyBindingEvent: function(keyBindings, event) {
456        if (this.stopKeyboardEventPropagation) {
457          event.stopPropagation();
458        }
459
460        // if event has been already prevented, don't do anything
461        if (event.defaultPrevented) {
462          return;
463        }
464
465        for (var i = 0; i < keyBindings.length; i++) {
466          var keyCombo = keyBindings[i][0];
467          var handlerName = keyBindings[i][1];
468          if (keyComboMatchesEvent(keyCombo, event)) {
469            this._triggerKeyHandler(keyCombo, handlerName, event);
470            // exit the loop if eventDefault was prevented
471            if (event.defaultPrevented) {
472              return;
473            }
474          }
475        }
476      },
477
478      _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
479        var detail = Object.create(keyCombo);
480        detail.keyboardEvent = keyboardEvent;
481        var event = new CustomEvent(keyCombo.event, {
482          detail: detail,
483          cancelable: true
484        });
485        this[handlerName].call(this, event);
486        if (event.defaultPrevented) {
487          keyboardEvent.preventDefault();
488        }
489      }
490    };
491  })();
492</script>
493