Skip to content

Commit 4f7cb77

Browse files
authored
Custom shortkey manager (codesandbox#378)
* Create custom shortkey manager * Add stronger checks for double tapping * Fix keymap updating
1 parent b6b7d3e commit 4f7cb77

File tree

5 files changed

+121
-51
lines changed

5 files changed

+121
-51
lines changed

packages/app/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@
134134
"moment": "^2.18.1",
135135
"monaco-editor": "CompuIves/codesandbox-monaco-editor",
136136
"monaco-vue": "^0.1.0",
137-
"mousetrap": "^1.6.1",
138137
"normalize.css": "^5.0.0",
139138
"normalizr": "^3.2.3",
140139
"postcss": "^6.0.9",

packages/app/src/app/components/Preference/PreferenceKeybinding/KeybindingInput.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React from 'react';
22

33
import Input from 'app/components/Input';
44

5+
import { normalizeKey } from 'app/store/preferences/keybindings';
6+
57
type Props = {
68
value: Array<string>,
79
setValue: (Array<string>) => any,
@@ -28,14 +30,6 @@ function formatKey(key: string) {
2830
}
2931
}
3032

31-
function normalizeKey(e: KeyboardEvent) {
32-
if (e.code.startsWith('Key')) {
33-
return String.fromCharCode(e.keyCode);
34-
}
35-
36-
return e.key;
37-
}
38-
3933
function sortKeys(keys: Array<string>) {
4034
return keys.sort((a, b) => {
4135
const isASpecial = SPECIAL_KEYS.indexOf(a) > -1;

packages/app/src/app/containers/KeybindingManager.js

Lines changed: 106 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import React from 'react';
1+
import React, { KeyboardEvent } from 'react';
22
import { connect } from 'react-redux';
33
import { createSelector } from 'reselect';
4-
import Mousetrap from 'mousetrap';
5-
import 'mousetrap/plugins/global-bind/mousetrap-global-bind.min';
6-
import 'mousetrap/plugins/pause/mousetrap-pause.min';
74

8-
import { KEYBINDINGS } from 'app/store/preferences/keybindings';
5+
import { KEYBINDINGS, normalizeKey } from 'app/store/preferences/keybindings';
96
import { keybindingsSelector } from 'app/store/preferences/selectors';
107
import { modalSelector } from 'app/store/modal/selectors';
118

@@ -17,62 +14,138 @@ type Props = {
1714
action: Function,
1815
},
1916
},
17+
bindingStrings: {
18+
[combo: string]: string,
19+
},
2020
dispatch: Function,
2121
modalOpen: boolean,
2222
};
2323

2424
const mapStateToProps = createSelector(
2525
keybindingsSelector,
26-
() => KEYBINDINGS,
2726
modalSelector,
28-
(userKeybindings, defaultBindings, modal) => {
29-
const newBindings = { ...defaultBindings };
27+
(userKeybindings, modal) => {
28+
const newBindings = { ...KEYBINDINGS };
3029
Object.keys(userKeybindings).forEach(key => {
3130
newBindings[key].bindings = userKeybindings[key];
3231
});
3332

34-
return { keybindings: newBindings, modalOpen: modal.open };
33+
const bindingStrings = {};
34+
35+
Object.keys(newBindings).forEach(key => {
36+
const binding = newBindings[key];
37+
38+
if (binding.bindings[0]) {
39+
const bindingString = binding.bindings[0].join('');
40+
41+
if (binding.bindings[1] && binding.bindings[1].length) {
42+
bindingStrings[bindingString] = {
43+
[binding.bindings[1].join('')]: key,
44+
};
45+
} else {
46+
bindingStrings[bindingString] = key;
47+
}
48+
}
49+
});
50+
51+
return { keybindings: newBindings, bindingStrings, modalOpen: modal.open };
3552
}
3653
);
3754
const mapDispatchToProps = dispatch => ({
3855
dispatch,
3956
});
4057
class KeybindingManager extends React.Component<Props> {
41-
setBindings = () => {
42-
Mousetrap.reset();
43-
Object.keys(this.props.keybindings).forEach(k => {
44-
const { bindings, action } = this.props.keybindings[k]; // eslint-disable-line
45-
const stroke =
46-
bindings[0].join('+') +
47-
(bindings[1] && bindings[1].length ? ' ' + bindings[1].join('+') : '');
48-
49-
Mousetrap.bindGlobal(stroke.toLowerCase(), () => {
50-
this.props.dispatch(action({ id: this.props.sandboxId }));
51-
return false;
52-
});
53-
});
58+
pressedComboKeys = [];
59+
pressedComboMetaKeys = [];
60+
checkedStrokes = this.props.bindingStrings;
61+
62+
removeFromPressedComboKeys = (key: string) => {
63+
this.pressedComboKeys = this.pressedComboKeys.filter(x => x !== key);
5464
};
5565

56-
shouldComponentUpdate(nextProps: Props) {
57-
if (nextProps.modalOpen) {
58-
Mousetrap.pause();
59-
} else {
60-
Mousetrap.unpause();
66+
handleKeyDown = (e: KeyboardEvent) => {
67+
if (this.props.modalOpen) {
68+
return;
6169
}
6270

63-
return nextProps.keybindings !== this.props.keybindings;
71+
const key = normalizeKey(e);
72+
73+
if (this.pressedComboKeys.indexOf(key) === -1) {
74+
this.pressedComboKeys.push(key);
75+
76+
// if the meta key is pressed
77+
// register the keyCode also in seperate array
78+
if (e.metaKey) {
79+
this.pressedComboMetaKeys.push(key);
80+
}
81+
}
82+
83+
// check match
84+
const match = this.checkCombosForPressedKeys();
85+
86+
if (match != null) {
87+
e.preventDefault();
88+
e.stopPropagation();
89+
}
90+
91+
if (typeof match === 'string') {
92+
this.pressedComboKeys = [];
93+
this.pressedComboMetaKeys = [];
94+
this.checkedStrokes = this.props.bindingStrings;
95+
96+
this.props.dispatch(
97+
this.props.keybindings[match].action({
98+
id: this.props.sandboxId,
99+
})
100+
);
101+
} else if (typeof match === 'object') {
102+
this.checkedStrokes = match;
103+
104+
if (this.timeout) {
105+
clearTimeout(this.timeout);
106+
}
107+
108+
this.timeout = setTimeout(() => {
109+
this.checkedStrokes = this.props.bindingStrings;
110+
}, 300);
111+
}
112+
};
113+
114+
checkCombosForPressedKeys() {
115+
const pressedComboKeysStr = this.pressedComboKeys.join('');
116+
117+
return this.checkedStrokes[pressedComboKeysStr];
64118
}
65119

120+
handleKeyUp = (e: KeyboardEvent) => {
121+
const key = normalizeKey(e);
122+
123+
this.removeFromPressedComboKeys(key);
124+
if (this.pressedComboMetaKeys.length > 0) {
125+
// if there are keys that were pressed while
126+
// the meta key was pressed flush them
127+
// because the keyup wasn't triggered for them
128+
// @see http:// stackoverflow.com/questions/27380018/when-cmd-key-is-kept-pressed-keyup-is-not-triggered-for-any-other-key
129+
130+
this.pressedComboMetaKeys.forEach(metaKey =>
131+
this.removeFromPressedComboKeys(metaKey)
132+
);
133+
this.pressedComboMetaKeys = [];
134+
}
135+
};
136+
66137
componentWillMount() {
67-
this.setBindings();
138+
document.addEventListener('keydown', this.handleKeyDown);
139+
document.addEventListener('keyup', this.handleKeyUp);
68140
}
69141

70-
componentDidUpdate() {
71-
this.setBindings();
142+
componentWillUnmount() {
143+
document.removeEventListener('keydown', this.handleKeyDown);
144+
document.removeEventListener('keyup', this.handleKeyUp);
72145
}
73146

74-
componentWillUnmount() {
75-
Mousetrap.reset();
147+
componentDidUpdate() {
148+
this.checkedStrokes = this.props.bindingStrings;
76149
}
77150

78151
render() {

packages/app/src/app/store/preferences/keybindings.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import {
1010
const isMac = !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i);
1111
const metaKey = isMac ? 'Meta' : 'Alt';
1212

13+
export function normalizeKey(e: KeyboardEvent) {
14+
if (e.code.startsWith('Key')) {
15+
return String.fromCharCode(e.keyCode);
16+
}
17+
18+
return e.key;
19+
}
20+
1321
export const KEYBINDINGS = {
1422
'editor.workspace': {
1523
title: 'Toggle Sidebar',
@@ -22,31 +30,31 @@ export const KEYBINDINGS = {
2230

2331
'editor.editor-mode': {
2432
title: 'Editor View',
25-
bindings: [[metaKey, 'K'], ['E']],
33+
bindings: [[metaKey, 'K', 'E']],
2634
action: ({ id }) => (dispatch: Function) => {
2735
dispatch(sandboxActions.setViewMode(id, true, false));
2836
},
2937
},
3038

3139
'editor.preview-mode': {
3240
title: 'Preview View',
33-
bindings: [[metaKey, 'K'], ['P']],
41+
bindings: [[metaKey, 'K', 'P']],
3442
action: ({ id }) => (dispatch: Function) => {
3543
dispatch(sandboxActions.setViewMode(id, false, true));
3644
},
3745
},
3846

3947
'editor.split-mode': {
4048
title: 'Split View',
41-
bindings: [[metaKey, 'K'], ['S']],
49+
bindings: [[metaKey, 'K', 'S']],
4250
action: ({ id }) => (dispatch: Function) => {
4351
dispatch(sandboxActions.setViewMode(id, true, true));
4452
},
4553
},
4654

4755
'editor.zen-mode': {
4856
title: 'Zen Mode',
49-
bindings: [[metaKey, 'K'], ['Z']],
57+
bindings: [[metaKey, 'K', 'Z']],
5058
action: () => (dispatch: Function, getState: Function) => {
5159
const currentZenMode = preferencesSelector(getState()).zenMode;
5260
dispatch(
@@ -59,7 +67,7 @@ export const KEYBINDINGS = {
5967

6068
'editor.toggle-console': {
6169
title: 'Toggle Console',
62-
bindings: [[metaKey, 'K'], ['D']],
70+
bindings: [[metaKey, 'K', 'D']],
6371
action: () => (dispatch: Function, getState: Function) => {
6472
const devToolsOpen = devToolsOpenSelector(getState());
6573
dispatch(viewActions.setDevToolsOpen(!devToolsOpen));

yarn.lock

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9387,10 +9387,6 @@ monaco-vue@^0.1.0:
93879387
version "0.1.0"
93889388
resolved "https://registry.yarnpkg.com/monaco-vue/-/monaco-vue-0.1.0.tgz#e6879a4f0d7aba3fcf499a6401d51e8e147b6046"
93899389

9390-
mousetrap@^1.6.1:
9391-
version "1.6.1"
9392-
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9"
9393-
93949390
move-concurrently@^1.0.1:
93959391
version "1.0.1"
93969392
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"

0 commit comments

Comments
 (0)