5 * Chrome uses an older version of DOM Level 3 Keyboard Events
7 * Most keys are labeled as text, but some are Unicode codepoints.
8 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
10 var KEY_IDENTIFIER
= {
55 * Special table for KeyboardEvent.keyCode.
56 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better
59 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
79 * MODIFIER_KEYS maps the short name for modifier keys used in a key
80 * combo string to the property name that references those same keys
81 * in a KeyboardEvent instance.
91 * KeyboardEvent.key is mostly represented by printable character made by
92 * the keyboard, with unprintable keys labeled nicely.
94 * However, on OS X, Alt+char can make a Unicode character that follows an
95 * Apple-specific mapping. In this case, we
96 * fall back to .keyCode.
98 var KEY_CHAR
= /[a-z0-9*]/;
101 * Matches a keyIdentifier string.
103 var IDENT_CHAR
= /U\+/;
106 * Matches arrow keys in Gecko 27.0+
108 var ARROW_KEY
= /^arrow/;
111 * Matches space keys everywhere (notably including IE10's exceptional name
114 var SPACE_KEY
= /^space(bar)?/;
116 function transformKey(key
) {
119 var lKey
= key
.toLowerCase();
120 if (lKey
.length
== 1) {
121 if (KEY_CHAR
.test(lKey
)) {
124 } else if (ARROW_KEY
.test(lKey
)) {
125 validKey
= lKey
.replace('arrow', '');
126 } else if (SPACE_KEY
.test(lKey
)) {
128 } else if (lKey
== 'multiply') {
129 // numpad '*' can map to Multiply on IE/Windows
138 function transformKeyIdentifier(keyIdent
) {
141 if (IDENT_CHAR
.test(keyIdent
)) {
142 validKey
= KEY_IDENTIFIER
[keyIdent
];
144 validKey
= keyIdent
.toLowerCase();
150 function transformKeyCode(keyCode
) {
152 if (Number(keyCode
)) {
153 if (keyCode
>= 65 && keyCode
<= 90) {
155 // lowercase is 32 offset from uppercase
156 validKey
= String
.fromCharCode(32 + keyCode
);
157 } else if (keyCode
>= 112 && keyCode
<= 123) {
158 // function keys f1-f12
159 validKey
= 'f' + (keyCode
- 112);
160 } else if (keyCode
>= 48 && keyCode
<= 57) {
162 validKey
= String(48 - keyCode
);
163 } else if (keyCode
>= 96 && keyCode
<= 105) {
165 validKey
= String(96 - keyCode
);
167 validKey
= KEY_CODE
[keyCode
];
173 function normalizedKeyForEvent(keyEvent
) {
174 // fall back from .key, to .keyIdentifier, to .keyCode, and then to
175 // .detail.key to support artificial keyboard events
176 return transformKey(keyEvent
.key
) ||
177 transformKeyIdentifier(keyEvent
.keyIdentifier
) ||
178 transformKeyCode(keyEvent
.keyCode
) ||
179 transformKey(keyEvent
.detail
.key
) || '';
182 function keyComboMatchesEvent(keyCombo
, keyEvent
) {
183 return normalizedKeyForEvent(keyEvent
) === keyCombo
.key
&&
184 !!keyEvent
.shiftKey
=== !!keyCombo
.shiftKey
&&
185 !!keyEvent
.ctrlKey
=== !!keyCombo
.ctrlKey
&&
186 !!keyEvent
.altKey
=== !!keyCombo
.altKey
&&
187 !!keyEvent
.metaKey
=== !!keyCombo
.metaKey
;
190 function parseKeyComboString(keyComboString
) {
191 return keyComboString
.split('+').reduce(function(parsedKeyCombo
, keyComboPart
) {
192 var eventParts
= keyComboPart
.split(':');
193 var keyName
= eventParts
[0];
194 var event
= eventParts
[1];
196 if (keyName
in MODIFIER_KEYS
) {
197 parsedKeyCombo
[MODIFIER_KEYS
[keyName
]] = true;
199 parsedKeyCombo
.key
= keyName
;
200 parsedKeyCombo
.event
= event
|| 'keydown';
203 return parsedKeyCombo
;
205 combo
: keyComboString
.split(':').shift()
209 function parseEventString(eventString
) {
210 return eventString
.split(' ').map(function(keyComboString
) {
211 return parseKeyComboString(keyComboString
);
217 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
218 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
219 * The element takes care of browser differences with respect to Keyboard events
220 * and uses an expressive syntax to filter key presses.
222 * Use the `keyBindings` prototype property to express what combination of keys
223 * will trigger the event to fire.
225 * Use the `key-event-target` attribute to set up event handlers on a specific
227 * The `keys-pressed` event will fire when one of the key combinations set with the
228 * `keys` property is pressed.
230 * @demo demo/index.html
231 * @polymerBehavior IronA11yKeysBehavior
233 Polymer
.IronA11yKeysBehavior
= {
236 * The HTMLElement that will be firing relevant KeyboardEvents.
252 // We use this due to a limitation in IE10 where instances will have
253 // own properties of everything on the "prototype".
254 _imperativeKeyBindings
: {
263 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
268 registered: function() {
269 this._prepKeyBindings();
272 attached: function() {
273 this._listenKeyEventListeners();
276 detached: function() {
277 this._unlistenKeyEventListeners();
281 * Can be used to imperatively add a key binding to the implementing
282 * element. This is the imperative equivalent of declaring a keybinding
283 * in the `keyBindings` prototype property.
285 addOwnKeyBinding: function(eventString
, handlerName
) {
286 this._imperativeKeyBindings
[eventString
] = handlerName
;
287 this._prepKeyBindings();
288 this._resetKeyEventListeners();
292 * When called, will remove all imperatively-added key bindings.
294 removeOwnKeyBindings: function() {
295 this._imperativeKeyBindings
= {};
296 this._prepKeyBindings();
297 this._resetKeyEventListeners();
300 keyboardEventMatchesKeys: function(event
, eventString
) {
301 var keyCombos
= parseEventString(eventString
);
304 for (index
= 0; index
< keyCombos
.length
; ++index
) {
305 if (keyComboMatchesEvent(keyCombos
[index
], event
)) {
313 _collectKeyBindings: function() {
314 var keyBindings
= this.behaviors
.map(function(behavior
) {
315 return behavior
.keyBindings
;
318 if (keyBindings
.indexOf(this.keyBindings
) === -1) {
319 keyBindings
.push(this.keyBindings
);
325 _prepKeyBindings: function() {
326 this._keyBindings
= {};
328 this._collectKeyBindings().forEach(function(keyBindings
) {
329 for (var eventString
in keyBindings
) {
330 this._addKeyBinding(eventString
, keyBindings
[eventString
]);
334 for (var eventString
in this._imperativeKeyBindings
) {
335 this._addKeyBinding(eventString
, this._imperativeKeyBindings
[eventString
]);
339 _addKeyBinding: function(eventString
, handlerName
) {
340 parseEventString(eventString
).forEach(function(keyCombo
) {
341 this._keyBindings
[keyCombo
.event
] =
342 this._keyBindings
[keyCombo
.event
] || [];
344 this._keyBindings
[keyCombo
.event
].push([
351 _resetKeyEventListeners: function() {
352 this._unlistenKeyEventListeners();
354 if (this.isAttached
) {
355 this._listenKeyEventListeners();
359 _listenKeyEventListeners: function() {
360 Object
.keys(this._keyBindings
).forEach(function(eventName
) {
361 var keyBindings
= this._keyBindings
[eventName
];
362 var boundKeyHandler
= this._onKeyBindingEvent
.bind(this, keyBindings
);
364 this._boundKeyHandlers
.push([this.keyEventTarget
, eventName
, boundKeyHandler
]);
366 this.keyEventTarget
.addEventListener(eventName
, boundKeyHandler
);
370 _unlistenKeyEventListeners: function() {
376 while (this._boundKeyHandlers
.length
) {
377 // My kingdom for block-scope binding and destructuring assignment..
378 keyHandlerTuple
= this._boundKeyHandlers
.pop();
379 keyEventTarget
= keyHandlerTuple
[0];
380 eventName
= keyHandlerTuple
[1];
381 boundKeyHandler
= keyHandlerTuple
[2];
383 keyEventTarget
.removeEventListener(eventName
, boundKeyHandler
);
387 _onKeyBindingEvent: function(keyBindings
, event
) {
388 keyBindings
.forEach(function(keyBinding
) {
389 var keyCombo
= keyBinding
[0];
390 var handlerName
= keyBinding
[1];
392 if (!event
.defaultPrevented
&& keyComboMatchesEvent(keyCombo
, event
)) {
393 this._triggerKeyHandler(keyCombo
, handlerName
, event
);
398 _triggerKeyHandler: function(keyCombo
, handlerName
, keyboardEvent
) {
399 var detail
= Object
.create(keyCombo
);
400 detail
.keyboardEvent
= keyboardEvent
;
402 this[handlerName
].call(this, new CustomEvent(keyCombo
.event
, {