6 * Chrome uses an older version of DOM Level 3 Keyboard Events
8 * Most keys are labeled as text, but some are Unicode codepoints.
9 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
11 var KEY_IDENTIFIER
= {
56 * Special table for KeyboardEvent.keyCode.
57 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better
60 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
80 * MODIFIER_KEYS maps the short name for modifier keys used in a key
81 * combo string to the property name that references those same keys
82 * in a KeyboardEvent instance.
92 * KeyboardEvent.key is mostly represented by printable character made by
93 * the keyboard, with unprintable keys labeled nicely.
95 * However, on OS X, Alt+char can make a Unicode character that follows an
96 * Apple-specific mapping. In this case, we
97 * fall back to .keyCode.
99 var KEY_CHAR
= /[a-z0-9*]/;
102 * Matches a keyIdentifier string.
104 var IDENT_CHAR
= /U\+/;
107 * Matches arrow keys in Gecko 27.0+
109 var ARROW_KEY
= /^arrow/;
112 * Matches space keys everywhere (notably including IE10's exceptional name
115 var SPACE_KEY
= /^space(bar)?/;
117 function transformKey(key
) {
120 var lKey
= key
.toLowerCase();
121 if (lKey
.length
== 1) {
122 if (KEY_CHAR
.test(lKey
)) {
125 } else if (ARROW_KEY
.test(lKey
)) {
126 validKey
= lKey
.replace('arrow', '');
127 } else if (SPACE_KEY
.test(lKey
)) {
129 } else if (lKey
== 'multiply') {
130 // numpad '*' can map to Multiply on IE/Windows
139 function transformKeyIdentifier(keyIdent
) {
142 if (IDENT_CHAR
.test(keyIdent
)) {
143 validKey
= KEY_IDENTIFIER
[keyIdent
];
145 validKey
= keyIdent
.toLowerCase();
151 function transformKeyCode(keyCode
) {
153 if (Number(keyCode
)) {
154 if (keyCode
>= 65 && keyCode
<= 90) {
156 // lowercase is 32 offset from uppercase
157 validKey
= String
.fromCharCode(32 + keyCode
);
158 } else if (keyCode
>= 112 && keyCode
<= 123) {
159 // function keys f1-f12
160 validKey
= 'f' + (keyCode
- 112);
161 } else if (keyCode
>= 48 && keyCode
<= 57) {
163 validKey
= String(48 - keyCode
);
164 } else if (keyCode
>= 96 && keyCode
<= 105) {
166 validKey
= String(96 - keyCode
);
168 validKey
= KEY_CODE
[keyCode
];
174 function normalizedKeyForEvent(keyEvent
) {
175 // fall back from .key, to .keyIdentifier, to .keyCode, and then to
176 // .detail.key to support artificial keyboard events
177 return transformKey(keyEvent
.key
) ||
178 transformKeyIdentifier(keyEvent
.keyIdentifier
) ||
179 transformKeyCode(keyEvent
.keyCode
) ||
180 transformKey(keyEvent
.detail
.key
) || '';
183 function keyComboMatchesEvent(keyCombo
, keyEvent
) {
184 return normalizedKeyForEvent(keyEvent
) === keyCombo
.key
&&
185 !!keyEvent
.shiftKey
=== !!keyCombo
.shiftKey
&&
186 !!keyEvent
.ctrlKey
=== !!keyCombo
.ctrlKey
&&
187 !!keyEvent
.altKey
=== !!keyCombo
.altKey
&&
188 !!keyEvent
.metaKey
=== !!keyCombo
.metaKey
;
191 function parseKeyComboString(keyComboString
) {
192 return keyComboString
.split('+').reduce(function(parsedKeyCombo
, keyComboPart
) {
193 var eventParts
= keyComboPart
.split(':');
194 var keyName
= eventParts
[0];
195 var event
= eventParts
[1];
197 if (keyName
in MODIFIER_KEYS
) {
198 parsedKeyCombo
[MODIFIER_KEYS
[keyName
]] = true;
200 parsedKeyCombo
.key
= keyName
;
201 parsedKeyCombo
.event
= event
|| 'keydown';
204 return parsedKeyCombo
;
206 combo
: keyComboString
.split(':').shift()
210 function parseEventString(eventString
) {
211 return eventString
.split(' ').map(function(keyComboString
) {
212 return parseKeyComboString(keyComboString
);
218 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
219 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
220 * The element takes care of browser differences with respect to Keyboard events
221 * and uses an expressive syntax to filter key presses.
223 * Use the `keyBindings` prototype property to express what combination of keys
224 * will trigger the event to fire.
226 * Use the `key-event-target` attribute to set up event handlers on a specific
228 * The `keys-pressed` event will fire when one of the key combinations set with the
229 * `keys` property is pressed.
231 * @demo demo/index.html
232 * @polymerBehavior IronA11yKeysBehavior
234 Polymer
.IronA11yKeysBehavior
= {
237 * The HTMLElement that will be firing relevant KeyboardEvents.
253 // We use this due to a limitation in IE10 where instances will have
254 // own properties of everything on the "prototype".
255 _imperativeKeyBindings
: {
264 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
269 registered: function() {
270 this._prepKeyBindings();
273 attached: function() {
274 this._listenKeyEventListeners();
277 detached: function() {
278 this._unlistenKeyEventListeners();
282 * Can be used to imperatively add a key binding to the implementing
283 * element. This is the imperative equivalent of declaring a keybinding
284 * in the `keyBindings` prototype property.
286 addOwnKeyBinding: function(eventString
, handlerName
) {
287 this._imperativeKeyBindings
[eventString
] = handlerName
;
288 this._prepKeyBindings();
289 this._resetKeyEventListeners();
293 * When called, will remove all imperatively-added key bindings.
295 removeOwnKeyBindings: function() {
296 this._imperativeKeyBindings
= {};
297 this._prepKeyBindings();
298 this._resetKeyEventListeners();
301 keyboardEventMatchesKeys: function(event
, eventString
) {
302 var keyCombos
= parseEventString(eventString
);
305 for (index
= 0; index
< keyCombos
.length
; ++index
) {
306 if (keyComboMatchesEvent(keyCombos
[index
], event
)) {
314 _collectKeyBindings: function() {
315 var keyBindings
= this.behaviors
.map(function(behavior
) {
316 return behavior
.keyBindings
;
319 if (keyBindings
.indexOf(this.keyBindings
) === -1) {
320 keyBindings
.push(this.keyBindings
);
326 _prepKeyBindings: function() {
327 this._keyBindings
= {};
329 this._collectKeyBindings().forEach(function(keyBindings
) {
330 for (var eventString
in keyBindings
) {
331 this._addKeyBinding(eventString
, keyBindings
[eventString
]);
335 for (var eventString
in this._imperativeKeyBindings
) {
336 this._addKeyBinding(eventString
, this._imperativeKeyBindings
[eventString
]);
340 _addKeyBinding: function(eventString
, handlerName
) {
341 parseEventString(eventString
).forEach(function(keyCombo
) {
342 this._keyBindings
[keyCombo
.event
] =
343 this._keyBindings
[keyCombo
.event
] || [];
345 this._keyBindings
[keyCombo
.event
].push([
352 _resetKeyEventListeners: function() {
353 this._unlistenKeyEventListeners();
355 if (this.isAttached
) {
356 this._listenKeyEventListeners();
360 _listenKeyEventListeners: function() {
361 Object
.keys(this._keyBindings
).forEach(function(eventName
) {
362 var keyBindings
= this._keyBindings
[eventName
];
363 var boundKeyHandler
= this._onKeyBindingEvent
.bind(this, keyBindings
);
365 this._boundKeyHandlers
.push([this.keyEventTarget
, eventName
, boundKeyHandler
]);
367 this.keyEventTarget
.addEventListener(eventName
, boundKeyHandler
);
371 _unlistenKeyEventListeners: function() {
377 while (this._boundKeyHandlers
.length
) {
378 // My kingdom for block-scope binding and destructuring assignment..
379 keyHandlerTuple
= this._boundKeyHandlers
.pop();
380 keyEventTarget
= keyHandlerTuple
[0];
381 eventName
= keyHandlerTuple
[1];
382 boundKeyHandler
= keyHandlerTuple
[2];
384 keyEventTarget
.removeEventListener(eventName
, boundKeyHandler
);
388 _onKeyBindingEvent: function(keyBindings
, event
) {
389 keyBindings
.forEach(function(keyBinding
) {
390 var keyCombo
= keyBinding
[0];
391 var handlerName
= keyBinding
[1];
393 if (!event
.defaultPrevented
&& keyComboMatchesEvent(keyCombo
, event
)) {
394 this._triggerKeyHandler(keyCombo
, handlerName
, event
);
399 _triggerKeyHandler: function(keyCombo
, handlerName
, keyboardEvent
) {
400 var detail
= Object
.create(keyCombo
);
401 detail
.keyboardEvent
= keyboardEvent
;
403 this[handlerName
].call(this, new CustomEvent(keyCombo
.event
, {