├── .editorconfig ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── SYNTAX.md ├── example ├── index.js └── package.json ├── index.js ├── package.json ├── src ├── codes.js ├── format_code.js ├── is_input.js ├── match.js ├── parse_code.js ├── parse_events.js └── platform.js ├── syntax.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [**.js] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test.js 2 | example 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: 5 | - npm install -g npm@~1.4.6 6 | before_script: 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | script: 10 | - npm run test-ff 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.1.1 2 | 3 | * make 'keybindingsOnInputs' opt-in, the default is again that inputs do not trigger keybindings 4 | 5 | ## 2.1.0 6 | 7 | * allow keybinding while focused on an input via keybindingsOnInputs 8 | * extend support for React 0.14 9 | * remove dependency on react to peerDependencies 10 | 11 | ## 2.0.1 12 | 13 | * Fix `formatCode` with shifted keys: they now display as the input, like +, 14 | rather than the required keys, like shift+= 15 | 16 | ## 1.2.0 17 | 18 | * Implement keybinding formatter that simplifies and shortens longform 19 | keycodes for display in interfaces. 20 | 21 | ## 1.1.1 22 | 23 | * Never push `undefined` onto the array of keybindings. This is useful 24 | if you have a component that reads keybindings but doesn't have any of its own. 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This code is licensed under the MIT license. 2 | 3 | Copyright © 2013, iD authors. 4 | 5 | Portions copyright © 2011, Keith Cirkel 6 | See https://github.com/keithamus/jwerty 7 | 8 | Portions of Mousetrap.js licensed Apache 2.0 (c) Craig Campbell 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-keybinding 2 | 3 | [![build status](https://secure.travis-ci.org/mapbox/react-keybinding.png)](http://travis-ci.org/mapbox/react-keybinding) 4 | 5 | Declarative, lightweight, and robust keybindings mixin for React. 6 | 7 | * Straightforward `'⌘S'` string syntax for declaring bindings 8 | * Automatically binds & unbinds keybindings when components mount and unmount 9 | * Allows listing of all currently-active keybindings 10 | * Run a function when a keybinding is hit or pass an action 11 | to the `keybinding` method of that component 12 | * Doesn't fire keybindings accidentally triggered in inputs, 13 | select boxes, or textareas. 14 | * Optionally coerce platform specific keybindings (i.e. `'⌘S'` on Mac to `'^S'` on Windows) 15 | 16 | ## Installation 17 | 18 | Install with [npm](https://www.npmjs.com/) and use in your React 19 | projects with either [browserify](http://browserify.org/) or 20 | [webpack](http://webpack.github.io/). 21 | 22 | ```sh 23 | $ npm install react-keybinding 24 | ``` 25 | 26 | ## Example 27 | 28 | ```js 29 | var React = require('react'), 30 | Keybinding = require('../'); 31 | var HelloMessage = React.createClass({ 32 | mixins: [Keybinding], 33 | keybindingsPlatformAgnostic: true, 34 | keybindings: { 35 | '⌘S': function(e) { 36 | console.log('save!'); 37 | e.preventDefault(); 38 | }, 39 | '⌘C': 'COPY' 40 | }, 41 | keybinding: function(event, action) { 42 | // event is the browser event, action is 'COPY' 43 | console.log(arguments); 44 | }, 45 | render: function() { 46 | return React.createElement("div", null, "Hello"); 47 | } 48 | }); 49 | React.render(React.createElement(HelloMessage, {name: "John"}), document.body); 50 | ``` 51 | 52 | There's a runnable example in the `./examples` directory: to run it, 53 | 54 | ```sh 55 | $ npm install 56 | $ cd example 57 | $ npm install 58 | $ npm start 59 | ``` 60 | 61 | See [tmcw/ditty](https://github.com/tmcw/ditty) for an example of 62 | react-keybinding in an application. 63 | 64 | ### API 65 | 66 | This module exposes a single mixin called `Keybinding`. 67 | 68 | Where you use this mixin on Components, it expects a property called 69 | `keybindings` of the format: 70 | 71 | ```js 72 | keybindings: { 73 | // keys are strings: they can contain meta and shift symbols, 74 | // numbers, strings, etc 75 | '⌘S': function(e) { 76 | // bindings can map to functions that they call directly 77 | }, 78 | // or to constants that are passed to the component's 79 | // 'keybinding' method. 80 | '⌘C': 'COPY' 81 | } 82 | ``` 83 | 84 | Platform agnostic keybindings will automatically listen for the `'Ctrl'` 85 | equivalent of `'Cmd'` keybindings, and vice-versa. To automatically coerce 86 | platform specific keybindings, provide a property called 87 | `keybindingsPlatformAgnostic` of the format: 88 | 89 | ```js 90 | keybindingsPlatformAgnostic: true, 91 | keybindings: { ... } 92 | ``` 93 | 94 | The mixin provides a method for components called `.getAllKeybindings()`: 95 | this yields an array of all `keybindings` properties on all active components. 96 | 97 | ## [Syntax](SYNTAX.md) 98 | 99 | The full [range of codes and modifiers supported is listed in SYNTAX.md](SYNTAX.md). 100 | 101 | ## Tests 102 | 103 | ```sh 104 | $ npm test 105 | ``` 106 | -------------------------------------------------------------------------------- /SYNTAX.md: -------------------------------------------------------------------------------- 1 | # Syntax 2 | 3 | ## keyCodes 4 | 5 | | input | keyCode | 6 | |------------|------------------| 7 | | `` 0 `` | 48 | 8 | | `` 1 `` | 49 | 9 | | `` 2 `` | 50 | 10 | | `` 3 `` | 51 | 11 | | `` 4 `` | 52 | 12 | | `` 5 `` | 53 | 13 | | `` 6 `` | 54 | 14 | | `` 7 `` | 55 | 15 | | `` 8 `` | 56 | 16 | | `` 9 `` | 57 | 17 | | `` ⌫ `` | 8 | 18 | | `` backspace `` | 8 | 19 | | `` ⇥ `` | 9 | 20 | | `` ⇆ `` | 9 | 21 | | `` tab `` | 9 | 22 | | `` ↩ `` | 13 | 23 | | `` return `` | 13 | 24 | | `` enter `` | 13 | 25 | | `` ⌅ `` | 13 | 26 | | `` pause `` | 19 | 27 | | `` pause-break `` | 19 | 28 | | `` ⇪ `` | 20 | 29 | | `` caps `` | 20 | 30 | | `` caps-lock `` | 20 | 31 | | `` ⎋ `` | 27 | 32 | | `` escape `` | 27 | 33 | | `` esc `` | 27 | 34 | | `` space `` | 32 | 35 | | `` ↖ `` | 33 | 36 | | `` pgup `` | 33 | 37 | | `` page-up `` | 33 | 38 | | `` ↘ `` | 34 | 39 | | `` pgdown `` | 34 | 40 | | `` page-down `` | 34 | 41 | | `` ⇟ `` | 35 | 42 | | `` end `` | 35 | 43 | | `` ⇞ `` | 36 | 44 | | `` home `` | 36 | 45 | | `` ins `` | 45 | 46 | | `` insert `` | 45 | 47 | | `` ⌦ `` | 46 | 48 | | `` del `` | 46 | 49 | | `` delete `` | 46 | 50 | | `` ← `` | 37 | 51 | | `` left `` | 37 | 52 | | `` arrow-left `` | 37 | 53 | | `` ↑ `` | 38 | 54 | | `` up `` | 38 | 55 | | `` arrow-up `` | 38 | 56 | | `` → `` | 39 | 57 | | `` right `` | 39 | 58 | | `` arrow-right `` | 39 | 59 | | `` ↓ `` | 40 | 60 | | `` down `` | 40 | 61 | | `` arrow-down `` | 40 | 62 | | `` * `` | 106 | 63 | | `` star `` | 106 | 64 | | `` asterisk `` | 106 | 65 | | `` multiply `` | 106 | 66 | | `` + `` | 107 | 67 | | `` plus `` | 107 | 68 | | `` - `` | 109 | 69 | | `` subtract `` | 109 | 70 | | `` ; `` | 186 | 71 | | `` semicolon `` | 186 | 72 | | `` = `` | 187 | 73 | | `` equals `` | 187 | 74 | | `` , `` | 188 | 75 | | `` comma `` | 188 | 76 | | `` . `` | 190 | 77 | | `` period `` | 190 | 78 | | `` full-stop `` | 190 | 79 | | `` / `` | 191 | 80 | | `` slash `` | 191 | 81 | | `` forward-slash `` | 191 | 82 | | `` ` `` | 192 | 83 | | `` tick `` | 192 | 84 | | `` back-quote `` | 192 | 85 | | `` [ `` | 219 | 86 | | `` open-bracket `` | 219 | 87 | | `` \ `` | 220 | 88 | | `` back-slash `` | 220 | 89 | | `` ] `` | 221 | 90 | | `` close-bracket `` | 221 | 91 | | `` ' `` | 222 | 92 | | `` quote `` | 222 | 93 | | `` apostrophe `` | 222 | 94 | | `` num-0 `` | 96 | 95 | | `` num-1 `` | 97 | 96 | | `` num-2 `` | 98 | 97 | | `` num-3 `` | 99 | 98 | | `` num-4 `` | 100 | 99 | | `` num-5 `` | 101 | 100 | | `` num-6 `` | 102 | 101 | | `` num-7 `` | 103 | 102 | | `` num-8 `` | 104 | 103 | | `` num-9 `` | 105 | 104 | | `` f1 `` | 112 | 105 | | `` f2 `` | 113 | 106 | | `` f3 `` | 114 | 107 | | `` f4 `` | 115 | 108 | | `` f5 `` | 116 | 109 | | `` f6 `` | 117 | 110 | | `` f7 `` | 118 | 111 | | `` f8 `` | 119 | 112 | | `` f9 `` | 120 | 113 | | `` f10 `` | 121 | 114 | | `` f11 `` | 122 | 115 | | `` f12 `` | 123 | 116 | | `` f13 `` | 124 | 117 | | `` f14 `` | 125 | 118 | | `` f15 `` | 126 | 119 | | `` f16 `` | 127 | 120 | | `` f17 `` | 128 | 121 | | `` f18 `` | 129 | 122 | | `` f19 `` | 130 | 123 | | `` f20 `` | 131 | 124 | | `` f21 `` | 132 | 125 | | `` f22 `` | 133 | 126 | | `` f23 `` | 134 | 127 | | `` f24 `` | 135 | 128 | | `` @ `` | 64 | 129 | | `` a `` | 65 | 130 | | `` b `` | 66 | 131 | | `` c `` | 67 | 132 | | `` d `` | 68 | 133 | | `` e `` | 69 | 134 | | `` f `` | 70 | 135 | | `` g `` | 71 | 136 | | `` h `` | 72 | 137 | | `` i `` | 73 | 138 | | `` j `` | 74 | 139 | | `` k `` | 75 | 140 | | `` l `` | 76 | 141 | | `` m `` | 77 | 142 | | `` n `` | 78 | 143 | | `` o `` | 79 | 144 | | `` p `` | 80 | 145 | | `` q `` | 81 | 146 | | `` r `` | 82 | 147 | | `` s `` | 83 | 148 | | `` t `` | 84 | 149 | | `` u `` | 85 | 150 | | `` v `` | 86 | 151 | | `` w `` | 87 | 152 | | `` x `` | 88 | 153 | | `` y `` | 89 | 154 | | `` z `` | 90 | 155 | 156 | 157 | ## shift key combinations 158 | 159 | | input | keyCode | 160 | |------------|------------------| 161 | | `` ~ `` | shift + 192 | 162 | | `` ! `` | shift + 49 | 163 | | `` @ `` | shift + 50 | 164 | | `` # `` | shift + 51 | 165 | | `` $ `` | shift + 52 | 166 | | `` % `` | shift + 53 | 167 | | `` ^ `` | shift + 54 | 168 | | `` & `` | shift + 55 | 169 | | `` * `` | shift + 56 | 170 | | `` ( `` | shift + 57 | 171 | | `` ) `` | shift + 48 | 172 | | `` _ `` | shift + 109 | 173 | | `` + `` | shift + 187 | 174 | | `` : `` | shift + 186 | 175 | | `` " `` | shift + 222 | 176 | | `` < `` | shift + 188 | 177 | | `` > `` | shift + 190 | 178 | | `` ? `` | shift + 191 | 179 | 180 | 181 | ## modifiers 182 | 183 | | input | keyCode | 184 | |------------|------------------| 185 | | `` ⇧ `` | 16 | 186 | | `` shift `` | 16 | 187 | | `` ⌃ `` | 17 | 188 | | `` ctrl `` | 17 | 189 | | `` ⌥ `` | 18 | 190 | | `` alt `` | 18 | 191 | | `` option `` | 18 | 192 | | `` ⌘ `` | 91 | 193 | | `` meta `` | 91 | 194 | | `` cmd `` | 91 | 195 | | `` super `` | 91 | 196 | | `` win `` | 91 | 197 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | Keybinding = require('../'); 3 | 4 | var HelloMessage = React.createClass({ 5 | mixins: [Keybinding], 6 | keybindings: { 7 | '⌘S': function(e) { 8 | console.log('save!'); 9 | e.preventDefault(); 10 | }, 11 | '⌘C': 'COPY' 12 | }, 13 | keybinding: function(event, action) { 14 | // event is the browser event 15 | // action is 'COPY' 16 | console.log(arguments); 17 | }, 18 | render: function() { 19 | return React.createElement("div", null, "Hello"); 20 | } 21 | }); 22 | 23 | React.render(React.createElement(HelloMessage, {name: "John"}), document.body); 24 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo index.js --outfile bundle.js --verbose --live" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "budo": "^0.1.12", 13 | "watchify": "^2.3.0" 14 | }, 15 | "dependencies": { 16 | "react": "^0.12.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | parseEvents = require('./src/parse_events.js'), 3 | isInput = require('./src/is_input.js'), 4 | match = require('./src/match.js'); 5 | 6 | /** 7 | * A React mixin that provides keybinding support for components 8 | */ 9 | var Keybinding = { 10 | 11 | /** 12 | * Housekeeping to pass around a single array of all 13 | * currently-active keybinding objects. 14 | */ 15 | childContextTypes: { 16 | __keybindings: React.PropTypes.array 17 | }, 18 | contextTypes: { 19 | __keybindings: React.PropTypes.array 20 | }, 21 | getChildContext: function() { 22 | return { __keybindings: this.__getKeybindings() }; 23 | }, 24 | __getKeybindings: function() { 25 | this.__keybindings = this.__keybindings || 26 | (this.context && this.context.__keybindings) || []; 27 | return this.__keybindings; 28 | }, 29 | 30 | /** 31 | * This is the only method meant to be exposed to the user: it 32 | * returns the global keybinding index for the purposes of documentation 33 | * generation. 34 | */ 35 | getAllKeybindings: function() { 36 | return this.__getKeybindings(); 37 | }, 38 | 39 | /** 40 | * Internal method: avoids firing keybindings in textareas, 41 | * figures out if they match any of the bindings from this component, 42 | * and then either fires an inline method or the .keybinding() method. 43 | */ 44 | __keybinding: function(event) { 45 | if (isInput(event) && !this.keybindingsOnInputs) return; 46 | for (var i = 0; i < this.matchers.length; i++) { 47 | if (match(this.matchers[i].expectation, event)) { 48 | if (typeof this.matchers[i].action === 'function') { 49 | this.matchers[i].action.apply(this, [event]); 50 | } else { 51 | if (typeof this.keybinding !== 'function') { 52 | throw new Error('non-function keybinding action given but no .keybinding method found on component'); 53 | } 54 | this.keybinding(event, this.matchers[i].action); 55 | } 56 | } 57 | } 58 | }, 59 | 60 | /** 61 | * When the component mounts, bind our event listener and 62 | * add our keybindings to the global index. 63 | */ 64 | componentDidMount: function() { 65 | if (this.keybindings !== undefined) { 66 | this.matchers = parseEvents(this.keybindings, !!this.keybindingsPlatformAgnostic); 67 | this.__boundKeybinding = this.__keybinding.bind(this); 68 | document.addEventListener('keydown', this.__boundKeybinding); 69 | this.__getKeybindings().push(this.keybindings); 70 | } 71 | }, 72 | 73 | /** 74 | * When the component unmounts, unbind our event listener and 75 | * remove our keybindings from the global index. 76 | */ 77 | componentWillUnmount: function() { 78 | if (this.keybindings !== undefined && this.__boundKeybinding !== undefined) { 79 | document.removeEventListener('keydown', this.__boundKeybinding); 80 | this.__getKeybindings() 81 | .splice(this.__getKeybindings().indexOf(this.keybindings), 1); 82 | } 83 | } 84 | }; 85 | 86 | module.exports = Keybinding; 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "peerDependencies": { 3 | "react": ">=0.13.0 <16.0.0" 4 | }, 5 | "name": "@mapbox/react-keybinding", 6 | "license": "MIT", 7 | "author": "Tom MacWright", 8 | "repository": { 9 | "url": "git@github.com:mapbox/react-keybinding.git", 10 | "type": "git" 11 | }, 12 | "version": "3.0.0", 13 | "scripts": { 14 | "test": "browserify test.js | tap-closer | smokestack", 15 | "test-ff": "node test.js && browserify test.js | tap-closer | smokestack -b firefox" 16 | }, 17 | "keywords": [ 18 | "keybinding", 19 | "keybindings", 20 | "react", 21 | "react-component" 22 | ], 23 | "devDependencies": { 24 | "react": "^0.13.3", 25 | "tap-closer": "^1.0.0", 26 | "tape": "^3.5.0", 27 | "happen": "^0.1.3", 28 | "smokestack": "^3.2.0", 29 | "browserify": "^9.0.3" 30 | }, 31 | "main": "index.js", 32 | "description": "declarative, concise keybindings for react" 33 | } 34 | -------------------------------------------------------------------------------- /src/codes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Keycodes, modifier codes, 3 | * and anything else that can be typed, 4 | * compiled into a big lookup table. 5 | */ 6 | module.exports.modifierCodes = { 7 | // Shift key, ⇧ 8 | '⇧': 16, shift: 16, 9 | // CTRL key, on Mac: ⌃ 10 | '⌃': 17, ctrl: 17, 11 | // ALT key, on Mac: ⌥ (Alt) 12 | '⌥': 18, alt: 18, option: 18, 13 | // META, on Mac: ⌘ (CMD), on Windows (Win), on Linux (Super) 14 | '⌘': 91, meta: 91, cmd: 91, 'super': 91, win: 91 15 | }; 16 | 17 | module.exports.modifierProperties = { 18 | 16: 'shiftKey', 19 | 17: 'ctrlKey', 20 | 18: 'altKey', 21 | 91: 'metaKey' 22 | }; 23 | 24 | var keyCodes = { 25 | // Backspace key, on Mac: ⌫ (Backspace) 26 | '⌫': 8, backspace: 8, 27 | // Tab Key, on Mac: ⇥ (Tab), on Windows ⇥⇥ 28 | '⇥': 9, '⇆': 9, tab: 9, 29 | // Return key, ↩ 30 | '↩': 13, 'return': 13, enter: 13, '⌅': 13, 31 | // Pause/Break key 32 | 'pause': 19, 'pause-break': 19, 33 | // Caps Lock key, ⇪ 34 | '⇪': 20, caps: 20, 'caps-lock': 20, 35 | // Escape key, on Mac: ⎋, on Windows: Esc 36 | '⎋': 27, escape: 27, esc: 27, 37 | // Space key 38 | space: 32, 39 | // Page-Up key, or pgup, on Mac: ↖ 40 | '↖': 33, pgup: 33, 'page-up': 33, 41 | // Page-Down key, or pgdown, on Mac: ↘ 42 | '↘': 34, pgdown: 34, 'page-down': 34, 43 | // END key, on Mac: ⇟ 44 | '⇟': 35, end: 35, 45 | // HOME key, on Mac: ⇞ 46 | '⇞': 36, home: 36, 47 | // Insert key, or ins 48 | ins: 45, insert: 45, 49 | // Delete key, on Mac: ⌦ (Delete) 50 | '⌦': 46, del: 46, 'delete': 46, 51 | // Left Arrow Key, or ← 52 | '←': 37, left: 37, 'arrow-left': 37, 53 | // Up Arrow Key, or ↑ 54 | '↑': 38, up: 38, 'arrow-up': 38, 55 | // Right Arrow Key, or → 56 | '→': 39, right: 39, 'arrow-right': 39, 57 | // Up Arrow Key, or ↓ 58 | '↓': 40, down: 40, 'arrow-down': 40, 59 | // odities, printing characters that come out wrong: 60 | // Num-Multiply, or * 61 | '*': 106, star: 106, asterisk: 106, multiply: 106, 62 | // Num-Plus or + 63 | '+': 107, 'plus': 107, 64 | // Num-Subtract, or - 65 | '-': 109, subtract: 109, 66 | // Semicolon 67 | ';': 186, semicolon:186, 68 | // = or equals 69 | '=': 187, 'equals': 187, 70 | // Comma, or , 71 | ',': 188, comma: 188, 72 | //'-': 189, //??? 73 | // Period, or ., or full-stop 74 | '.': 190, period: 190, 'full-stop': 190, 75 | // Slash, or /, or forward-slash 76 | '/': 191, slash: 191, 'forward-slash': 191, 77 | // Tick, or `, or back-quote 78 | '`': 192, tick: 192, 'back-quote': 192, 79 | // Open bracket, or [ 80 | '[': 219, 'open-bracket': 219, 81 | // Back slash, or \ 82 | '\\': 220, 'back-slash': 220, 83 | // Close backet, or ] 84 | ']': 221, 'close-bracket': 221, 85 | // Apostrophe, or Quote, or ' 86 | '\'': 222, quote: 222, apostrophe: 222 87 | }; 88 | 89 | // NUMPAD 0-9 90 | var i = 95, n = 0; 91 | while (++i < 106) { 92 | keyCodes['num-' + n] = i; 93 | ++n; 94 | } 95 | 96 | // 0-9 97 | i = 47; n = 0; 98 | while (++i < 58) { 99 | keyCodes[n] = i; ++n; 100 | } 101 | 102 | // F1-F25 103 | i = 111; n = 1; 104 | while (++i < 136) { 105 | keyCodes['f' + n] = i; ++n; 106 | } 107 | 108 | // ;-a-z 109 | i = 63; 110 | while (++i < 91) { 111 | keyCodes[String.fromCharCode(i).toLowerCase()] = i; 112 | } 113 | 114 | // these non-letter keys imply a shift key on US keyboards 115 | var shiftEquivalents = [[ '~', '`' ], 116 | [ '!', '1' ], 117 | [ '@', '2' ], 118 | [ '#', '3' ], 119 | [ '$', '4' ], 120 | [ '%', '5' ], 121 | [ '^', '6' ], 122 | [ '&', '7' ], 123 | [ '*', '8' ], 124 | [ '(', '9' ], 125 | [ ')', '0' ], 126 | [ '_', '-' ], 127 | [ '+', '=' ], 128 | [ ':', ';' ], 129 | [ '\"', '\'' ], 130 | [ '<', ',' ], 131 | [ '>', '.' ], 132 | [ '?', '/' ] 133 | ]; 134 | 135 | module.exports.unshiftedKeys = shiftEquivalents.reduce(function(memo, key) { 136 | memo[keyCodes[key[1]]] = key[0]; 137 | return memo; 138 | }, {}); 139 | 140 | module.exports.shiftedKeys = shiftEquivalents.reduce(function(shiftedKeys, key) { 141 | shiftedKeys[key[0]] = keyCodes[key[1]]; 142 | return shiftedKeys; 143 | }, {}); 144 | 145 | module.exports.keyCodes = keyCodes; 146 | -------------------------------------------------------------------------------- /src/format_code.js: -------------------------------------------------------------------------------- 1 | var codes = require('./codes.js'); 2 | 3 | function findShortestIdentifiers(set) { 4 | var reversed = {}, k; 5 | for (k in set) { 6 | var id = set[k]; 7 | if (reversed[id] === undefined) reversed[id] = []; 8 | reversed[id].push(k); 9 | } 10 | function shorter(a, b) { 11 | return a.length - b.length; 12 | } 13 | for (k in reversed) { 14 | reversed[k] = reversed[k].sort(shorter)[0]; 15 | } 16 | return reversed; 17 | } 18 | 19 | var shortest = { 20 | modifierCodes: findShortestIdentifiers(codes.modifierCodes), 21 | keyCodes: findShortestIdentifiers(codes.keyCodes) 22 | }; 23 | 24 | /** 25 | * Format a key code combination into an array of shortest-possible 26 | * key identifiers 27 | * @param {Object} input parsed keybinding 28 | * @returns {Array} array of identifiers 29 | * @example 30 | * formatCode(parseCode('a')); 31 | */ 32 | module.exports = function formatCode(input) { 33 | var formatted = []; 34 | var isShifted = input.keyCode in codes.unshiftedKeys; 35 | if (input.shiftKey && !isShifted) { 36 | formatted.push(shortest.modifierCodes[16]); 37 | } 38 | if (input.metaKey) formatted.push(shortest.modifierCodes[91]); 39 | if (input.altKey) formatted.push(shortest.modifierCodes[18]); 40 | if (input.ctrlKey) formatted.push(shortest.modifierCodes[17]); 41 | 42 | if (input.keyCode !== null) { 43 | if (isShifted) { 44 | formatted.push(codes.unshiftedKeys[input.keyCode]); 45 | } else { 46 | var shortCode = shortest.keyCodes[input.keyCode]; 47 | formatted.push(shortCode ? shortCode : input.keyCode); 48 | } 49 | } 50 | 51 | return formatted; 52 | }; 53 | -------------------------------------------------------------------------------- /src/is_input.js: -------------------------------------------------------------------------------- 1 | var blacklist = { 2 | INPUT: true, 3 | SELECT: true, 4 | TEXTAREA: true 5 | }; 6 | 7 | module.exports = function(event) { 8 | return blacklist[event.target.tagName]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/match.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Verify if an event checks all the boxes 3 | * of an expectation generated by `parse_event.js` 4 | * @param {Object} expectation 5 | * @param {Event} DOMEvent 6 | * @returns {boolean} whether the event matches 7 | */ 8 | module.exports = function(expectation, event) { 9 | for (var p in expectation) { 10 | if (event[p] != expectation[p]) { return false; } 11 | } 12 | return true; 13 | }; 14 | -------------------------------------------------------------------------------- /src/parse_code.js: -------------------------------------------------------------------------------- 1 | var codes = require('./codes.js'); 2 | 3 | /** 4 | * parse a key code given as a string 5 | * into an object of expectations used 6 | * to match keystrokes. 7 | * @param {String} input a string of a keycode combination 8 | * @returns {Object} expectations 9 | * @example 10 | * parseCode('a'); 11 | */ 12 | module.exports = function parseCode(input) { 13 | 14 | var code = input.toLowerCase() 15 | .match(/(?:(?:[^+⇧⌃⌥⌘])+|[⇧⌃⌥⌘]|\+\+|^\+$)/g); 16 | 17 | var event = { 18 | keyCode: null, 19 | shiftKey: false, 20 | ctrlKey: false, 21 | altKey: false, 22 | metaKey: false 23 | }; 24 | 25 | for (var i = 0; i < code.length; i++) { 26 | // Normalise matching errors 27 | if (code[i] === '++') code[i] = '+'; 28 | 29 | if (code[i] in codes.modifierCodes) { 30 | event[codes.modifierProperties[codes.modifierCodes[code[i]]]] = true; 31 | } else if (code[i] in codes.shiftedKeys) { 32 | event.keyCode = codes.shiftedKeys[code[i]]; 33 | event.shiftKey = true; 34 | } else if (code[i] in codes.keyCodes) { 35 | event.keyCode = codes.keyCodes[code[i]]; 36 | } 37 | } 38 | 39 | return event; 40 | }; 41 | -------------------------------------------------------------------------------- /src/parse_events.js: -------------------------------------------------------------------------------- 1 | var parseCode = require('./parse_code.js'); 2 | 3 | /** 4 | * Given an object of mappings from 5 | * keybinding strings to actions, 6 | * return an array of parsed keystroke 7 | * expectations and actions. 8 | * @param {Object} keybindings 9 | * @param {Boolean} platformAgnostic convert platform specific keybindgs 10 | * @returns {Array} matchers 11 | */ 12 | module.exports = function(keybindings, platformAgnostic) { 13 | var matchers = []; 14 | for (var code in keybindings) { 15 | var event = parseCode(code); 16 | matchers.push({ 17 | expectation: event, 18 | action: keybindings[code] 19 | }); 20 | if (platformAgnostic && (event.metaKey ? !event.ctrlKey : event.ctrlKey)) { 21 | // meta xor ctrl 22 | matchers.push({ 23 | expectation: { 24 | keyCode: event.keyCode, 25 | shiftKey: event.shiftKey, 26 | ctrlKey: !event.ctrlKey, 27 | altKey: event.altKey, 28 | metaKey: !event.metaKey 29 | }, 30 | action: keybindings[code] 31 | }); 32 | } 33 | } 34 | return matchers; 35 | }; 36 | -------------------------------------------------------------------------------- /src/platform.js: -------------------------------------------------------------------------------- 1 | module.exports = function platform(code, os) { 2 | if (os === 'mac') return code; 3 | 4 | var replacements = { 5 | '⌘': 'Ctrl', 6 | '⇧': 'Shift', 7 | '⌥': 'Alt', 8 | '⌫': 'Backspace', 9 | '⌦': 'Delete' 10 | }, keys = []; 11 | 12 | if (os === 'win') { 13 | if (code === '⌘⇧Z') return 'Ctrl+Y'; 14 | } 15 | 16 | for (var i = 0; i < code.length; i++) { 17 | if (code[i] in replacements) { 18 | keys.push(replacements[code[i]]); 19 | } else { 20 | keys.push(code[i]); 21 | } 22 | } 23 | 24 | return keys.join('+'); 25 | } 26 | -------------------------------------------------------------------------------- /syntax.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var codes = require('./src/codes'); 3 | 4 | /** 5 | * This file generates the file SYNTAX.md, which lists 6 | * all of the available key combinations supported by this mixin. 7 | */ 8 | function pairs(o) { 9 | return Object.keys(o).map(function(k) { return [k, o[k]]; }); 10 | } 11 | 12 | var out = fs.createWriteStream('SYNTAX.md', { flags: 'w' }); 13 | 14 | out.write('# Syntax\n\n'); 15 | 16 | out.write('## keyCodes\n\n'); 17 | out.write('| input | keyCode |\n'); 18 | out.write('|------------|------------------|\n'); 19 | 20 | pairs(codes.keyCodes).forEach(function(pair) { 21 | out.write('| `` ' + pair[0] + ' `` | ' + pair[1] + ' |\n'); 22 | }); 23 | 24 | out.write('\n\n## shift key combinations\n\n'); 25 | out.write('| input | keyCode |\n'); 26 | out.write('|------------|------------------|\n'); 27 | 28 | pairs(codes.shiftedKeys).forEach(function(pair) { 29 | out.write('| `` ' + pair[0] + ' `` | shift + ' + pair[1] + ' |\n'); 30 | }); 31 | 32 | out.write('\n\n## modifiers\n\n'); 33 | out.write('| input | keyCode |\n'); 34 | out.write('|------------|------------------|\n'); 35 | 36 | pairs(codes.modifierCodes).forEach(function(pair) { 37 | out.write('| `` ' + pair[0] + ' `` | ' + pair[1] + ' |\n'); 38 | }); 39 | 40 | out.end(); 41 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | 3 | var formatCode = require('./src/format_code.js'); 4 | var parseCode = require('./src/parse_code.js'); 5 | 6 | test('formatCode', function(t) { 7 | t.deepEqual(formatCode(parseCode('shift')), ['⇧'], 'shift'); 8 | t.deepEqual(formatCode(parseCode('shift+a')), ['⇧', 'a'], 'modifier and char'); 9 | t.deepEqual(formatCode(parseCode('+')), ['+'], 'shifted key'); 10 | t.deepEqual(formatCode(parseCode('*')), ['*'], 'shifted key'); 11 | t.deepEqual(formatCode(parseCode('^')), ['^'], 'shifted key'); 12 | t.deepEqual(formatCode(parseCode('cmd+^')), [ '⌘', '^' ], 'cmd+shifted key'); 13 | t.deepEqual(formatCode(parseCode('shift+cmd')), ['⇧', '⌘'], 'multiple modifiers'); 14 | t.deepEqual(formatCode(parseCode('arrow-up')), ['↑'], 'longcode'); 15 | t.end(); 16 | }); 17 | 18 | test('parseCode', function(t) { 19 | t.deepEqual(parseCode('a'), { 20 | altKey: false, ctrlKey: false, keyCode: 65, metaKey: false, shiftKey: false }, 'a'); 21 | t.deepEqual(parseCode('b'), { 22 | altKey: false, ctrlKey: false, keyCode: 66, metaKey: false, shiftKey: false }, 'b'); 23 | t.deepEqual(parseCode('['), { 24 | altKey: false, ctrlKey: false, keyCode: 219, metaKey: false, shiftKey: false }, '['); 25 | t.deepEqual(parseCode('⌘V'), { 26 | altKey: false, ctrlKey: false, keyCode: 86, metaKey: true, shiftKey: false }, 'command c'); 27 | t.deepEqual(parseCode('⇧a'), { 28 | altKey: false, ctrlKey: false, keyCode: 65, metaKey: false, shiftKey: true }, 'shift a'); 29 | t.deepEqual(parseCode('⎋'), { 30 | altKey: false, ctrlKey: false, keyCode: 27, metaKey: false, shiftKey: false }, 'escape'); 31 | t.deepEqual(parseCode('?'), { 32 | altKey: false, ctrlKey: false, keyCode: 191, metaKey: false, shiftKey: true }, '?'); 33 | t.end(); 34 | }); 35 | 36 | var match = require('./src/match.js'); 37 | 38 | test('match', function(t) { 39 | t.equal( 40 | match( 41 | { altKey: false, ctrlKey: false, keyCode: 65, metaKey: false, shiftKey: false }, 42 | { altKey: false, ctrlKey: false, keyCode: 65, metaKey: false, shiftKey: false } 43 | ), true); 44 | t.equal( 45 | match( 46 | { altKey: false, ctrlKey: false, keyCode: 65, metaKey: false, shiftKey: false }, 47 | { altKey: true, ctrlKey: false, keyCode: 65, metaKey: false, shiftKey: false } 48 | ), false); 49 | t.end(); 50 | }); 51 | 52 | var parseEvents = require('./src/parse_events.js'); 53 | 54 | test('parseEvents', function(t) { 55 | t.deepEqual(parseEvents({'a':'b'}), [{ 56 | expectation: { 57 | altKey: false, ctrlKey: false, keyCode: 65, metaKey: false, shiftKey: false 58 | }, 59 | action: 'b' 60 | }], 'a'); 61 | t.end(); 62 | }); 63 | 64 | test('parseEvents platformAgnostic', function(t) { 65 | t.deepEqual(parseEvents({'a':'b'}, true), [{ 66 | expectation: { 67 | altKey: false, ctrlKey: false, keyCode: 65, metaKey: false, shiftKey: false 68 | }, 69 | action: 'b' 70 | }], 'ignore neither cmd nor ctrl'); 71 | t.deepEqual(parseEvents({'cmd+a':'b'}, true), [ 72 | { 73 | expectation: { 74 | altKey: false, ctrlKey: false, keyCode: 65, metaKey: true, shiftKey: false 75 | }, 76 | action: 'b' 77 | }, 78 | { 79 | expectation: { 80 | altKey: false, ctrlKey: true, keyCode: 65, metaKey: false, shiftKey: false 81 | }, 82 | action: 'b' 83 | } 84 | ], 'convert cmd to ctrl'); 85 | t.deepEqual(parseEvents({'ctrl+a':'b'}, true), [ 86 | { 87 | expectation: { 88 | altKey: false, ctrlKey: true, keyCode: 65, metaKey: false, shiftKey: false 89 | }, 90 | action: 'b' 91 | }, 92 | { 93 | expectation: { 94 | altKey: false, ctrlKey: false, keyCode: 65, metaKey: true, shiftKey: false 95 | }, 96 | action: 'b' 97 | } 98 | ], 'convert ctrl to cmd'); 99 | t.deepEqual(parseEvents({'cmd+ctrl+a':'b'}, true), [{ 100 | expectation: { 101 | altKey: false, ctrlKey: true, keyCode: 65, metaKey: true, shiftKey: false 102 | }, 103 | action: 'b' 104 | }], 'ignore both cmd and ctrl'); 105 | t.end(); 106 | }); 107 | 108 | var React = require('react/addons'), 109 | happen = require('happen'), 110 | TestUtils = React.addons.TestUtils; 111 | var Keybinding = require('./'); 112 | 113 | if (process.browser) { 114 | 115 | test('Keybinding: action', function(t) { 116 | var HelloMessage = React.createClass({ 117 | mixins: [Keybinding], 118 | keybindings: { 'C': 'COPY' }, 119 | keybinding: function(event, action) { 120 | t.equal(action, 'COPY'); 121 | t.equal(typeof event, 'object'); 122 | hello_message.componentWillUnmount(); 123 | t.end(); 124 | }, 125 | render: function() { return React.createElement('div', null); } 126 | }); 127 | 128 | var hello_message = TestUtils.renderIntoDocument( 129 | React.createElement(HelloMessage)); 130 | 131 | happen.once(document, { 132 | type: 'keydown', 133 | keyCode: 67 134 | }); 135 | }); 136 | 137 | test('Keybinding: action with meta', function(t) { 138 | var HelloMessage = React.createClass({ 139 | mixins: [Keybinding], 140 | keybindings: { 'cmd+C': 'COPY' }, 141 | keybinding: function(event, action) { 142 | t.equal(action, 'COPY'); 143 | t.equal(typeof event, 'object'); 144 | t.deepEqual(this.getAllKeybindings(), [{ 'cmd+C': 'COPY' }], 'getAllKeybindings'); 145 | hello_message.componentWillUnmount(); 146 | t.deepEqual(this.getAllKeybindings(), [], 'getAllKeybindings after unmount'); 147 | t.end(); 148 | }, 149 | render: function() { return React.createElement('div', null); } 150 | }); 151 | 152 | var hello_message = TestUtils.renderIntoDocument( 153 | React.createElement(HelloMessage)); 154 | 155 | happen.once(document, { 156 | type: 'keydown', 157 | keyCode: 67, 158 | metaKey: true 159 | }); 160 | }); 161 | 162 | test('Keybinding: ?', function(t) { 163 | var HelloMessage = React.createClass({ 164 | mixins: [Keybinding], 165 | keybindings: { '?': function() { t.pass(); t.end(); } }, 166 | render: function() { return React.createElement('div', null); } 167 | }); 168 | 169 | var hello_message = TestUtils.renderIntoDocument( 170 | React.createElement(HelloMessage)); 171 | 172 | happen.once(document, { type: 'keydown', keyCode: 191, shiftKey: true }); 173 | }); 174 | 175 | test('Keybinding: none by myself', function(t) { 176 | var HelloMessage = React.createClass({ 177 | mixins: [Keybinding], 178 | componentDidMount: function() { 179 | t.deepEqual(this.getAllKeybindings(), []); 180 | t.end(); 181 | }, 182 | render: function() { return React.createElement('div', null); } 183 | }); 184 | var hello_message = TestUtils.renderIntoDocument( 185 | React.createElement(HelloMessage)); 186 | }); 187 | 188 | } else { 189 | 190 | test('headless', function(t) { 191 | var HelloMessage = React.createClass({ 192 | mixins: [Keybinding], 193 | keybindings: { 'C': 'COPY' }, 194 | keybinding: function(event, action) { 195 | }, 196 | render: function() { return React.createElement('div', null); } 197 | }); 198 | t.ok(React.renderToString(React.createElement(HelloMessage))); 199 | t.end(); 200 | }); 201 | 202 | } 203 | --------------------------------------------------------------------------------