3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7 Code distributed by Google as part of the polymer project is also
8 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
11 <link rel=
"import" href=
"../polymer/polymer.html">
18 * Chrome uses an older version of DOM Level 3 Keyboard Events
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
23 var KEY_IDENTIFIER
= {
68 * Special table for KeyboardEvent.keyCode.
69 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better
72 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
92 * MODIFIER_KEYS maps the short name for modifier keys used in a key
93 * combo string to the property name that references those same keys
94 * in a KeyboardEvent instance.
104 * KeyboardEvent.key is mostly represented by printable character made by
105 * the keyboard, with unprintable keys labeled nicely.
107 * However, on OS X, Alt+char can make a Unicode character that follows an
108 * Apple-specific mapping. In this case, we
109 * fall back to .keyCode.
111 var KEY_CHAR
= /[a-z0-9*]/;
114 * Matches a keyIdentifier string.
116 var IDENT_CHAR
= /U\+/;
119 * Matches arrow keys in Gecko 27.0+
121 var ARROW_KEY
= /^arrow/;
124 * Matches space keys everywhere (notably including IE10's exceptional name
127 var SPACE_KEY
= /^space(bar)?/;
129 function transformKey(key
) {
132 var lKey
= key
.toLowerCase();
133 if (lKey
.length
== 1) {
134 if (KEY_CHAR
.test(lKey
)) {
137 } else if (ARROW_KEY
.test(lKey
)) {
138 validKey
= lKey
.replace('arrow', '');
139 } else if (SPACE_KEY
.test(lKey
)) {
141 } else if (lKey
== 'multiply') {
142 // numpad '*' can map to Multiply on IE/Windows
151 function transformKeyIdentifier(keyIdent
) {
154 if (IDENT_CHAR
.test(keyIdent
)) {
155 validKey
= KEY_IDENTIFIER
[keyIdent
];
157 validKey
= keyIdent
.toLowerCase();
163 function transformKeyCode(keyCode
) {
165 if (Number(keyCode
)) {
166 if (keyCode
>= 65 && keyCode
<= 90) {
168 // lowercase is 32 offset from uppercase
169 validKey
= String
.fromCharCode(32 + keyCode
);
170 } else if (keyCode
>= 112 && keyCode
<= 123) {
171 // function keys f1-f12
172 validKey
= 'f' + (keyCode
- 112);
173 } else if (keyCode
>= 48 && keyCode
<= 57) {
175 validKey
= String(48 - keyCode
);
176 } else if (keyCode
>= 96 && keyCode
<= 105) {
178 validKey
= String(96 - keyCode
);
180 validKey
= KEY_CODE
[keyCode
];
186 function normalizedKeyForEvent(keyEvent
) {
187 // fall back from .key, to .keyIdentifier, to .keyCode, and then to
188 // .detail.key to support artificial keyboard events
189 return transformKey(keyEvent
.key
) ||
190 transformKeyIdentifier(keyEvent
.keyIdentifier
) ||
191 transformKeyCode(keyEvent
.keyCode
) ||
192 transformKey(keyEvent
.detail
.key
) || '';
195 function keyComboMatchesEvent(keyCombo
, keyEvent
) {
196 return normalizedKeyForEvent(keyEvent
) === keyCombo
.key
&&
197 !!keyEvent
.shiftKey
=== !!keyCombo
.shiftKey
&&
198 !!keyEvent
.ctrlKey
=== !!keyCombo
.ctrlKey
&&
199 !!keyEvent
.altKey
=== !!keyCombo
.altKey
&&
200 !!keyEvent
.metaKey
=== !!keyCombo
.metaKey
;
203 function parseKeyComboString(keyComboString
) {
204 return keyComboString
.split('+').reduce(function(parsedKeyCombo
, keyComboPart
) {
205 var eventParts
= keyComboPart
.split(':');
206 var keyName
= eventParts
[0];
207 var event
= eventParts
[1];
209 if (keyName
in MODIFIER_KEYS
) {
210 parsedKeyCombo
[MODIFIER_KEYS
[keyName
]] = true;
212 parsedKeyCombo
.key
= keyName
;
213 parsedKeyCombo
.event
= event
|| 'keydown';
216 return parsedKeyCombo
;
218 combo
: keyComboString
.split(':').shift()
222 function parseEventString(eventString
) {
223 return eventString
.split(' ').map(function(keyComboString
) {
224 return parseKeyComboString(keyComboString
);
230 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
231 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
232 * The element takes care of browser differences with respect to Keyboard events
233 * and uses an expressive syntax to filter key presses.
235 * Use the `keyBindings` prototype property to express what combination of keys
236 * will trigger the event to fire.
238 * Use the `key-event-target` attribute to set up event handlers on a specific
240 * The `keys-pressed` event will fire when one of the key combinations set with the
241 * `keys` property is pressed.
243 * @demo demo/index.html
244 * @polymerBehavior IronA11yKeysBehavior
246 Polymer
.IronA11yKeysBehavior
= {
249 * The HTMLElement that will be firing relevant KeyboardEvents.
265 // We use this due to a limitation in IE10 where instances will have
266 // own properties of everything on the "prototype".
267 _imperativeKeyBindings
: {
276 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
281 registered: function() {
282 this._prepKeyBindings();
285 attached: function() {
286 this._listenKeyEventListeners();
289 detached: function() {
290 this._unlistenKeyEventListeners();
294 * Can be used to imperatively add a key binding to the implementing
295 * element. This is the imperative equivalent of declaring a keybinding
296 * in the `keyBindings` prototype property.
298 addOwnKeyBinding: function(eventString
, handlerName
) {
299 this._imperativeKeyBindings
[eventString
] = handlerName
;
300 this._prepKeyBindings();
301 this._resetKeyEventListeners();
305 * When called, will remove all imperatively-added key bindings.
307 removeOwnKeyBindings: function() {
308 this._imperativeKeyBindings
= {};
309 this._prepKeyBindings();
310 this._resetKeyEventListeners();
313 keyboardEventMatchesKeys: function(event
, eventString
) {
314 var keyCombos
= parseEventString(eventString
);
317 for (index
= 0; index
< keyCombos
.length
; ++index
) {
318 if (keyComboMatchesEvent(keyCombos
[index
], event
)) {
326 _collectKeyBindings: function() {
327 var keyBindings
= this.behaviors
.map(function(behavior
) {
328 return behavior
.keyBindings
;
331 if (keyBindings
.indexOf(this.keyBindings
) === -1) {
332 keyBindings
.push(this.keyBindings
);
338 _prepKeyBindings: function() {
339 this._keyBindings
= {};
341 this._collectKeyBindings().forEach(function(keyBindings
) {
342 for (var eventString
in keyBindings
) {
343 this._addKeyBinding(eventString
, keyBindings
[eventString
]);
347 for (var eventString
in this._imperativeKeyBindings
) {
348 this._addKeyBinding(eventString
, this._imperativeKeyBindings
[eventString
]);
352 _addKeyBinding: function(eventString
, handlerName
) {
353 parseEventString(eventString
).forEach(function(keyCombo
) {
354 this._keyBindings
[keyCombo
.event
] =
355 this._keyBindings
[keyCombo
.event
] || [];
357 this._keyBindings
[keyCombo
.event
].push([
364 _resetKeyEventListeners: function() {
365 this._unlistenKeyEventListeners();
367 if (this.isAttached
) {
368 this._listenKeyEventListeners();
372 _listenKeyEventListeners: function() {
373 Object
.keys(this._keyBindings
).forEach(function(eventName
) {
374 var keyBindings
= this._keyBindings
[eventName
];
375 var boundKeyHandler
= this._onKeyBindingEvent
.bind(this, keyBindings
);
377 this._boundKeyHandlers
.push([this.keyEventTarget
, eventName
, boundKeyHandler
]);
379 this.keyEventTarget
.addEventListener(eventName
, boundKeyHandler
);
383 _unlistenKeyEventListeners: function() {
389 while (this._boundKeyHandlers
.length
) {
390 // My kingdom for block-scope binding and destructuring assignment..
391 keyHandlerTuple
= this._boundKeyHandlers
.pop();
392 keyEventTarget
= keyHandlerTuple
[0];
393 eventName
= keyHandlerTuple
[1];
394 boundKeyHandler
= keyHandlerTuple
[2];
396 keyEventTarget
.removeEventListener(eventName
, boundKeyHandler
);
400 _onKeyBindingEvent: function(keyBindings
, event
) {
401 keyBindings
.forEach(function(keyBinding
) {
402 var keyCombo
= keyBinding
[0];
403 var handlerName
= keyBinding
[1];
405 if (!event
.defaultPrevented
&& keyComboMatchesEvent(keyCombo
, event
)) {
406 this._triggerKeyHandler(keyCombo
, handlerName
, event
);
411 _triggerKeyHandler: function(keyCombo
, handlerName
, keyboardEvent
) {
412 var detail
= Object
.create(keyCombo
);
413 detail
.keyboardEvent
= keyboardEvent
;
415 this[handlerName
].call(this, new CustomEvent(keyCombo
.event
, {