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