├── .gitignore ├── package.json ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .tern-* 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserkeymap", 3 | "version": "2.0.2", 4 | "description": "Map browser events to key names, and key names to values", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/marijnh/browserkeymap.git" 9 | }, 10 | "keywords": [ 11 | "browser", 12 | "key", 13 | "event", 14 | "keymap" 15 | ], 16 | "author": "Marijn Haverbeke ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/marijnh/browserkeymap/issues" 20 | }, 21 | "homepage": "https://github.com/marijnh/browserkeymap#readme" 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 by Marijn Haverbeke and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browser Keymap 2 | 3 | A small library for doing sane key binding in the browser. It defines two 4 | things: 5 | 6 | ### A string notation for key events 7 | 8 | `browserkeymap` keys are represented by strings like `"Shift-Space"`, 9 | `"Ctrl-Alt-Delete"`, or `"'x'"`. The rules are: 10 | 11 | * `keypress` events are represented as the character that was typed 12 | between single quotes. They do not support modifiers because 13 | browsers do not attach modifier information to `keypress` events. 14 | 15 | * `keydown` events are represented as zero or more modifiers 16 | (`Shift-`, `Cmd-`, `Alt-`, `Ctrl-`) followed by a key name. 17 | 18 | * A key name is a capital letter for a letter key, a digit for a 19 | number key, `F` plus a number for a function key, the symbol typed 20 | by the key when shift isn't held down (one of ``[\]`'*,-./;=``), or 21 | one of the names `Alt`, `Backspace`, `CapsLock`, `Ctrl`, `Delete`, 22 | `Down`, `End`, `Enter`, `Esc`, `Home`, `Insert`, `Left`, `Mod`, 23 | `PageDown`, `PageUp`, `Pause`, `PrintScrn`, `Right`, `Shift`, 24 | `Space`, `Tab`, or `Up`. 25 | 26 | You can get a key name from an event by calling 27 | `Keymap.keyName(event)`. 28 | 29 | You can normalize a key name string (fixing the order of the 30 | modifiers, and replacing alternative modifier names with their 31 | standard name) by calling `Keymap.normalizeKeyName(string)`. This 32 | function maps the modifier `Mod-` to `Cmd-` on Mac platforms and to 33 | `Ctrl-` on non-Mac platforms. It also accepts `a-` for `Alt-`, `c-` or 34 | `Control-` for `Ctrl-`, `m-` or `Meta-` for `Cmd-`, and `s-` for 35 | `Shift-`. 36 | 37 | You can use `Keymap.isModifierKey(string)` to find out whether the 38 | given key name refers to a modifier key. 39 | 40 | ### An object type for keymaps 41 | 42 | The `Keymap` constructor itself, which is the thing the library 43 | exports, can be used to build keymaps. 44 | 45 | var myMap = new Keymap({ 46 | "Ctrl-Q": handleQuit, 47 | "Shift-Space": autocomplete 48 | }) 49 | 50 | A keymap associates keys with values. You can call its `lookup` method 51 | to look up a key: 52 | 53 | myMap.lookup("Ctrl-Q") // → handleQuit 54 | myMap.lookup("Alt-F4") // → undefined 55 | 56 | You can create a new map from an existing map with its `update` 57 | method: 58 | 59 | var newMap = myMap.update({ 60 | "Alt-F4": handleQuit, 61 | "Ctrl-Q": null 62 | }) 63 | 64 | That will create a new map, starting with the bindings in `myMap`, 65 | adding a binding for `Alt-F4`, and removing the binding for `Ctrl-Q`. 66 | 67 | Multi-stroke keys are supported by providing space-separated names to 68 | a keymap. When a prefix of a multi-stroke key is looked up, the 69 | `lookup` method will return `Keymap.unfinished`. The handler should 70 | then buffer the key name, and on the next key event (possibly with a 71 | timeout to clear buffered keys), try again by prefixing the new key 72 | event's name with the buffered key(s). 73 | 74 | ## License 75 | 76 | This module is released under an MIT license. 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function(mod) { 2 | if (typeof exports == "object" && typeof module == "object") { // CommonJS 3 | module.exports = mod() 4 | module.exports.default = module.exports // ES6 modules compatibility 5 | } 6 | else if (typeof define == "function" && define.amd) // AMD 7 | return define([], mod) 8 | else // Plain browser env 9 | (this || window).browserKeymap = mod() 10 | })(function() { 11 | "use strict" 12 | 13 | var mac = typeof navigator != "undefined" ? /Mac/.test(navigator.platform) 14 | : typeof os != "undefined" ? os.platform() == "darwin" : false 15 | 16 | // :: Object 17 | // A map from key codes to key names. 18 | var keyNames = { 19 | 3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", 20 | 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", 21 | 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", 22 | 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 23 | 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete", 24 | 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 25 | 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", 26 | 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" 27 | } 28 | 29 | // Number keys 30 | for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i) 31 | // Alphabetic keys 32 | for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i) 33 | // Function keys 34 | for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i 35 | 36 | // :: (KeyboardEvent) → ?string 37 | // Find a name for the given keydown event. If the keycode in the 38 | // event is not known, this will return `null`. Otherwise, it will 39 | // return a string like `"Shift-Cmd-Ctrl-Alt-Home"`. The parts before 40 | // the dashes give the modifiers (always in that order, if present), 41 | // and the last word gives the key name, which one of the names in 42 | // `keyNames`. 43 | // 44 | // The convention for keypress events is to use the pressed character 45 | // between single quotes. Due to limitations in the browser API, 46 | // keypress events can not have modifiers. 47 | function keyName(event) { 48 | if (event.type == "keypress") return "'" + String.fromCharCode(event.charCode) + "'" 49 | 50 | var base = keyNames[event.keyCode], name = base 51 | if (name == null || event.altGraphKey) return null 52 | 53 | if (event.altKey && base != "Alt") name = "Alt-" + name 54 | if (event.ctrlKey && base != "Ctrl") name = "Ctrl-" + name 55 | if (event.metaKey && base != "Cmd") name = "Cmd-" + name 56 | if (event.shiftKey && base != "Shift") name = "Shift-" + name 57 | return name 58 | } 59 | 60 | // :: (string) → bool 61 | // Test whether the given key name refers to a modifier key. 62 | function isModifierKey(name) { 63 | name = /[^-]*$/.exec(name)[0] 64 | return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod" 65 | } 66 | 67 | // :: (string) → string 68 | // Normalize a sloppy key name, which may have modifiers in the wrong 69 | // order or use shorthands for modifiers, to a properly formed key 70 | // name. Used to normalize names provided in keymaps. 71 | // 72 | // Note that the modifier `mod` is a shorthand for `Cmd` on Mac, and 73 | // `Ctrl` on other platforms. 74 | function normalizeKeyName(name) { 75 | var parts = name.split(/-(?!'?$)/), result = parts[parts.length - 1] 76 | var alt, ctrl, shift, cmd 77 | for (var i = 0; i < parts.length - 1; i++) { 78 | var mod = parts[i] 79 | if (/^(cmd|meta|m)$/i.test(mod)) cmd = true 80 | else if (/^a(lt)?$/i.test(mod)) alt = true 81 | else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true 82 | else if (/^s(hift)?$/i.test(mod)) shift = true 83 | else if (/^mod$/i.test(mod)) { if (mac) cmd = true; else ctrl = true } 84 | else throw new Error("Unrecognized modifier name: " + mod) 85 | } 86 | if (alt) result = "Alt-" + result 87 | if (ctrl) result = "Ctrl-" + result 88 | if (cmd) result = "Cmd-" + result 89 | if (shift) result = "Shift-" + result 90 | return result 91 | } 92 | 93 | function hasProp(obj, prop) { 94 | return Object.prototype.hasOwnProperty.call(obj, prop) 95 | } 96 | 97 | function unusedMulti(bindings, name) { 98 | for (var binding in bindings) 99 | if (binding.length > name && binding.indexOf(name) == 0 && binding.charAt(name.length) == " ") 100 | return false 101 | return true 102 | } 103 | 104 | function updateBindings(bindings, base) { 105 | var result = {} 106 | if (base) for (var prop in base) if (hasProp(base, prop)) result[prop] = base[prop] 107 | 108 | for (var keyname in bindings) if (hasProp(bindings, keyname)) { 109 | var keys = keyname.split(/ +(?!\'$)/).map(normalizeKeyName) 110 | var value = bindings[keyname] 111 | if (value == null) { 112 | for (var i = keys.length - 1; i >= 0; i--) { 113 | var name = keys.slice(0, i + 1).join(" ") 114 | var old = result[name] 115 | if (old == Keymap.unfinished && !unusedMulti(result, name)) 116 | break 117 | else if (old) 118 | delete result[name] 119 | } 120 | } else { 121 | for (var i = 0; i < keys.length; i++) { 122 | var name = keys.slice(0, i + 1).join(" ") 123 | var val = i == keys.length - 1 ? value : Keymap.unfinished 124 | var prev = result[name] 125 | if (prev && (i < keys.length - 1 || prev == Keymap.unfinished) && prev != val) 126 | throw new Error("Inconsistent bindings for " + name) 127 | result[name] = val 128 | } 129 | } 130 | } 131 | return result 132 | } 133 | 134 | // :: (Object) → Keymap 135 | // A keymap binds a set of [key names](#keyName) to values. 136 | // 137 | // Construct a keymap using the given bindings, which should be an 138 | // object whose property names are [key names](#keyName) or 139 | // space-separated sequences of key names. In the second case, the 140 | // binding will be for a multi-stroke key combination. 141 | function Keymap(bindings, base) { 142 | this.bindings = updateBindings(bindings, base) 143 | } 144 | 145 | // :: (Object) → Keymap 146 | // Create a new keymap by adding bindings from the given object, 147 | // and removing the bindings that the object maps to null. 148 | Keymap.prototype.update = function(bindings) { 149 | return new Keymap(bindings, this.bindings) 150 | } 151 | 152 | // :: (string) → T 153 | // Looks up the given key or key sequence in this keymap. Returns 154 | // the value the key is bound to (which may be undefined if it is 155 | // not bound), or the value `Keymap.unfinished` if the key is a prefix of a 156 | // multi-key sequence that is bound by this keymap. 157 | Keymap.prototype.lookup = function(key) { 158 | return this.bindings[key] 159 | } 160 | 161 | Keymap.unfinished = {toString: function() { return "Keymap.unfinished" }} 162 | 163 | Keymap.keyName = keyName 164 | Keymap.isModifierKey = isModifierKey 165 | Keymap.normalizeKeyName = normalizeKeyName 166 | 167 | function ComputedKeymap(f) { this.f = f } 168 | ComputedKeymap.prototype.lookup = function(key, context) { return this.f(key, context) } 169 | // :: ((key: string, context: ?any) → T) → ComputedKeymap 170 | // Construct a 'computed' keymap from a function which takes a key 171 | // name and returns a binding. 172 | Keymap.Computed = ComputedKeymap 173 | 174 | return Keymap 175 | }) 176 | --------------------------------------------------------------------------------