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 .detail.key for artifical keyboard events,
180      // and then to deprecated .keyIdentifier and .keyCode.
181      if (keyEvent.key) {
182        return transformKey(keyEvent.key, noSpecialChars);
183      }
184      if (keyEvent.detail && keyEvent.detail.key) {
185        return transformKey(keyEvent.detail.key, noSpecialChars);
186      }
187      return transformKeyIdentifier(keyEvent.keyIdentifier) ||
188        transformKeyCode(keyEvent.keyCode) || '';
189    }
190
191    function keyComboMatchesEvent(keyCombo, event) {
192      // For combos with modifiers we support only alpha-numeric keys
193      var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
194      return keyEvent === keyCombo.key &&
195        (!keyCombo.hasModifiers || (
196          !!event.shiftKey === !!keyCombo.shiftKey &&
197          !!event.ctrlKey === !!keyCombo.ctrlKey &&
198          !!event.altKey === !!keyCombo.altKey &&
199          !!event.metaKey === !!keyCombo.metaKey)
200        );
201    }
202
203    function parseKeyComboString(keyComboString) {
204      if (keyComboString.length === 1) {
205        return {
206          combo: keyComboString,
207          key: keyComboString,
208          event: 'keydown'
209        };
210      }
211      return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) {
212        var eventParts = keyComboPart.split(':');
213        var keyName = eventParts[0];
214        var event = eventParts[1];
215
216        if (keyName in MODIFIER_KEYS) {
217          parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
218          parsedKeyCombo.hasModifiers = true;
219        } else {
220          parsedKeyCombo.key = keyName;
221          parsedKeyCombo.event = event || 'keydown';
222        }
223
224        return parsedKeyCombo;
225      }, {
226        combo: keyComboString.split(':').shift()
227      });
228    }
229
230    function parseEventString(eventString) {
231      return eventString.trim().split(' ').map(function(keyComboString) {
232        return parseKeyComboString(keyComboString);
233      });
234    }
235
236    /**
237     * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
238     * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
239     * The element takes care of browser differences with respect to Keyboard events
240     * and uses an expressive syntax to filter key presses.
241     *
242     * Use the `keyBindings` prototype property to express what combination of keys
243     * will trigger the callback. A key binding has the format
244     * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
245     * `"KEY:EVENT": "callback"` are valid as well). Some examples:
246     *
247     *      keyBindings: {
248     *        'space': '_onKeydown', // same as 'space:keydown'
249     *        'shift+tab': '_onKeydown',
250     *        'enter:keypress': '_onKeypress',
251     *        'esc:keyup': '_onKeyup'
252     *      }
253     *
254     * The callback will receive with an event containing the following information in `event.detail`:
255     *
256     *      _onKeydown: function(event) {
257     *        console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
258     *        console.log(event.detail.key); // KEY only, e.g. "tab"
259     *        console.log(event.detail.event); // EVENT, e.g. "keydown"
260     *        console.log(event.detail.keyboardEvent); // the original KeyboardEvent
261     *      }
262     *
263     * Use the `keyEventTarget` attribute to set up event handlers on a specific
264     * node.
265     *
266     * See the [demo source code](https://github.com/PolymerElements/iron-a11y-keys-behavior/blob/master/demo/x-key-aware.html)
267     * for an example.
268     *
269     * @demo demo/index.html
270     * @polymerBehavior
271     */
272    Polymer.IronA11yKeysBehavior = {
273      properties: {
274        /**
275         * The EventTarget that will be firing relevant KeyboardEvents. Set it to
276         * `null` to disable the listeners.
277         * @type {?EventTarget}
278         */
279        keyEventTarget: {
280          type: Object,
281          value: function() {
282            return this;
283          }
284        },
285
286        /**
287         * If true, this property will cause the implementing element to
288         * automatically stop propagation on any handled KeyboardEvents.
289         */
290        stopKeyboardEventPropagation: {
291          type: Boolean,
292          value: false
293        },
294
295        _boundKeyHandlers: {
296          type: Array,
297          value: function() {
298            return [];
299          }
300        },
301
302        // We use this due to a limitation in IE10 where instances will have
303        // own properties of everything on the "prototype".
304        _imperativeKeyBindings: {
305          type: Object,
306          value: function() {
307            return {};
308          }
309        }
310      },
311
312      observers: [
313        '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
314      ],
315
316
317      /**
318       * To be used to express what combination of keys  will trigger the relative
319       * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
320       * @type {!Object}
321       */
322      keyBindings: {},
323
324      registered: function() {
325        this._prepKeyBindings();
326      },
327
328      attached: function() {
329        this._listenKeyEventListeners();
330      },
331
332      detached: function() {
333        this._unlistenKeyEventListeners();
334      },
335
336      /**
337       * Can be used to imperatively add a key binding to the implementing
338       * element. This is the imperative equivalent of declaring a keybinding
339       * in the `keyBindings` prototype property.
340       */
341      addOwnKeyBinding: function(eventString, handlerName) {
342        this._imperativeKeyBindings[eventString] = handlerName;
343        this._prepKeyBindings();
344        this._resetKeyEventListeners();
345      },
346
347      /**
348       * When called, will remove all imperatively-added key bindings.
349       */
350      removeOwnKeyBindings: function() {
351        this._imperativeKeyBindings = {};
352        this._prepKeyBindings();
353        this._resetKeyEventListeners();
354      },
355
356      /**
357       * Returns true if a keyboard event matches `eventString`.
358       *
359       * @param {KeyboardEvent} event
360       * @param {string} eventString
361       * @return {boolean}
362       */
363      keyboardEventMatchesKeys: function(event, eventString) {
364        var keyCombos = parseEventString(eventString);
365        for (var i = 0; i < keyCombos.length; ++i) {
366          if (keyComboMatchesEvent(keyCombos[i], event)) {
367            return true;
368          }
369        }
370        return false;
371      },
372
373      _collectKeyBindings: function() {
374        var keyBindings = this.behaviors.map(function(behavior) {
375          return behavior.keyBindings;
376        });
377
378        if (keyBindings.indexOf(this.keyBindings) === -1) {
379          keyBindings.push(this.keyBindings);
380        }
381
382        return keyBindings;
383      },
384
385      _prepKeyBindings: function() {
386        this._keyBindings = {};
387
388        this._collectKeyBindings().forEach(function(keyBindings) {
389          for (var eventString in keyBindings) {
390            this._addKeyBinding(eventString, keyBindings[eventString]);
391          }
392        }, this);
393
394        for (var eventString in this._imperativeKeyBindings) {
395          this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]);
396        }
397
398        // Give precedence to combos with modifiers to be checked first.
399        for (var eventName in this._keyBindings) {
400          this._keyBindings[eventName].sort(function (kb1, kb2) {
401            var b1 = kb1[0].hasModifiers;
402            var b2 = kb2[0].hasModifiers;
403            return (b1 === b2) ? 0 : b1 ? -1 : 1;
404          })
405        }
406      },
407
408      _addKeyBinding: function(eventString, handlerName) {
409        parseEventString(eventString).forEach(function(keyCombo) {
410          this._keyBindings[keyCombo.event] =
411            this._keyBindings[keyCombo.event] || [];
412
413          this._keyBindings[keyCombo.event].push([
414            keyCombo,
415            handlerName
416          ]);
417        }, this);
418      },
419
420      _resetKeyEventListeners: function() {
421        this._unlistenKeyEventListeners();
422
423        if (this.isAttached) {
424          this._listenKeyEventListeners();
425        }
426      },
427
428      _listenKeyEventListeners: function() {
429        if (!this.keyEventTarget) {
430          return;
431        }
432        Object.keys(this._keyBindings).forEach(function(eventName) {
433          var keyBindings = this._keyBindings[eventName];
434          var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
435
436          this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
437
438          this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
439        }, this);
440      },
441
442      _unlistenKeyEventListeners: function() {
443        var keyHandlerTuple;
444        var keyEventTarget;
445        var eventName;
446        var boundKeyHandler;
447
448        while (this._boundKeyHandlers.length) {
449          // My kingdom for block-scope binding and destructuring assignment..
450          keyHandlerTuple = this._boundKeyHandlers.pop();
451          keyEventTarget = keyHandlerTuple[0];
452          eventName = keyHandlerTuple[1];
453          boundKeyHandler = keyHandlerTuple[2];
454
455          keyEventTarget.removeEventListener(eventName, boundKeyHandler);
456        }
457      },
458
459      _onKeyBindingEvent: function(keyBindings, event) {
460        if (this.stopKeyboardEventPropagation) {
461          event.stopPropagation();
462        }
463
464        // if event has been already prevented, don't do anything
465        if (event.defaultPrevented) {
466          return;
467        }
468
469        for (var i = 0; i < keyBindings.length; i++) {
470          var keyCombo = keyBindings[i][0];
471          var handlerName = keyBindings[i][1];
472          if (keyComboMatchesEvent(keyCombo, event)) {
473            this._triggerKeyHandler(keyCombo, handlerName, event);
474            // exit the loop if eventDefault was prevented
475            if (event.defaultPrevented) {
476              return;
477            }
478          }
479        }
480      },
481
482      _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
483        var detail = Object.create(keyCombo);
484        detail.keyboardEvent = keyboardEvent;
485        var event = new CustomEvent(keyCombo.event, {
486          detail: detail,
487          cancelable: true
488        });
489        this[handlerName].call(this, event);
490        if (event.defaultPrevented) {
491          keyboardEvent.preventDefault();
492        }
493      }
494    };
495  })();
496</script>
497