1 <!-- 2 @license 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 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