Skip to content

Commit ccb4265

Browse files
authored
Keymapping (codesandbox#377)
* Set Quick Command window under CMD+Shift+P * Keymapping * Fix lint errors
1 parent fb844e0 commit ccb4265

File tree

25 files changed

+699
-50
lines changed

25 files changed

+699
-50
lines changed

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
"moment": "^2.18.1",
135135
"monaco-editor": "CompuIves/codesandbox-monaco-editor",
136136
"monaco-vue": "^0.1.0",
137+
"mousetrap": "^1.6.1",
137138
"normalize.css": "^5.0.0",
138139
"normalizr": "^3.2.3",
139140
"postcss": "^6.0.9",
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import React from 'react';
2+
3+
import Input from 'app/components/Input';
4+
5+
type Props = {
6+
value: Array<string>,
7+
setValue: (Array<string>) => any,
8+
placeholder: string,
9+
style: ?Object,
10+
};
11+
12+
const SPECIAL_KEYS = ['Meta', 'Control', 'Alt', 'Shift', 'Enter', 'Backspace'];
13+
const IGNORED_KEYS = ['Backspace', 'Escape', 'CapsLock'];
14+
15+
function formatKey(key: string) {
16+
switch (key) {
17+
case 'Meta': {
18+
const isMac = !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i);
19+
if (isMac) {
20+
return '⌘';
21+
}
22+
return '⊞';
23+
}
24+
case 'Control':
25+
return 'Ctrl';
26+
default:
27+
return key;
28+
}
29+
}
30+
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+
39+
function sortKeys(keys: Array<string>) {
40+
return keys.sort((a, b) => {
41+
const isASpecial = SPECIAL_KEYS.indexOf(a) > -1;
42+
const isBSpecial = SPECIAL_KEYS.indexOf(b) > -1;
43+
44+
if (isASpecial && isBSpecial) {
45+
return 0;
46+
} else if (isASpecial) {
47+
return -1;
48+
} else if (isBSpecial) {
49+
return 1;
50+
}
51+
52+
return 0;
53+
});
54+
}
55+
56+
export default class KeybindingInput extends React.Component {
57+
props: Props;
58+
state = {
59+
recording: false,
60+
61+
recordedKeys: [],
62+
};
63+
64+
handleChange = e => {
65+
const value = e.target.value;
66+
67+
this.props.setValue(value);
68+
};
69+
70+
keypresses = 0;
71+
handleKeyDown = e => {
72+
e.preventDefault();
73+
e.stopPropagation();
74+
75+
if (e.key === 'Enter') {
76+
this.props.setValue(this.state.recordedKeys);
77+
} else if (e.key === 'Backspace') {
78+
this.props.setValue(undefined);
79+
}
80+
81+
if (e.key === 'Escape' || e.key === 'Enter' || e.key === 'Backspace') {
82+
this.setState({ recordedKeys: [] });
83+
e.target.blur();
84+
return;
85+
}
86+
87+
const upperCaseKey = normalizeKey(e);
88+
89+
if (
90+
this.state.recordedKeys.indexOf(upperCaseKey) === -1 &&
91+
IGNORED_KEYS.indexOf(e.key === -1)
92+
) {
93+
this.keypresses += 1;
94+
95+
this.setState({
96+
recordedKeys: sortKeys([...this.state.recordedKeys, upperCaseKey]),
97+
});
98+
}
99+
};
100+
101+
handleKeyUp = e => {
102+
e.preventDefault();
103+
e.stopPropagation();
104+
this.keypresses -= 1;
105+
};
106+
107+
handleKeyPress = e => {
108+
e.preventDefault();
109+
e.stopPropagation();
110+
};
111+
112+
handleFocus = () => {
113+
this.setState({
114+
recording: true,
115+
recordedKeys: [],
116+
});
117+
document.addEventListener('keydown', this.handleKeyDown);
118+
document.addEventListener('keyup', this.handleKeyUp);
119+
document.addEventListener('keypress', this.handleKeyPress);
120+
};
121+
122+
handleBlur = () => {
123+
this.keypresses = 0;
124+
if (this.state.recording) {
125+
this.setState({
126+
recording: false,
127+
});
128+
document.removeEventListener('keydown', this.handleKeyDown);
129+
document.removeEventListener('keyup', this.handleKeyUp);
130+
document.removeEventListener('keypress', this.handleKeyPress);
131+
}
132+
};
133+
134+
render() {
135+
const { recording, recordedKeys } = this.state;
136+
const { value, placeholder = 'Enter Keystroke' } = this.props;
137+
138+
const keys = recording ? recordedKeys : value || [];
139+
140+
return (
141+
<Input
142+
style={{ width: '6rem', ...this.props.style }}
143+
value={keys.map(formatKey).join(' + ')}
144+
placeholder={placeholder}
145+
onFocus={this.handleFocus}
146+
onBlur={this.handleBlur}
147+
readOnly
148+
/>
149+
);
150+
}
151+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
3+
import KeybindingInput from './KeybindingInput';
4+
5+
type Props = {
6+
value: {
7+
firstStroke: Array<string>,
8+
secondStroke: Array<string>,
9+
},
10+
setValue: Function,
11+
};
12+
13+
export default class PreferenceKeybinding extends React.PureComponent {
14+
props: Props;
15+
16+
setValue = (index: number) => (value: Array<string>) => {
17+
const result = [...this.props.value];
18+
result[index] = value;
19+
20+
this.props.setValue(result);
21+
};
22+
23+
render() {
24+
const { value } = this.props;
25+
26+
return (
27+
<div>
28+
<KeybindingInput
29+
{...this.props}
30+
placeholder="First"
31+
value={value[0]}
32+
setValue={this.setValue(0)}
33+
/>
34+
{' - '}
35+
<KeybindingInput
36+
{...this.props}
37+
placeholder="Second"
38+
value={value[1]}
39+
setValue={this.setValue(1)}
40+
disabled={!value[0] || value[0].length === 0}
41+
/>
42+
</div>
43+
);
44+
}
45+
}

packages/app/src/app/components/Preference/index.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import PreferenceSwitch from './PreferenceSwitch';
66
import PreferenceDropdown from './PreferenceDropdown';
77
import PreferenceNumber from './PreferenceNumber';
88
import PreferenceText from './PreferenceText';
9+
import PreferenceKeybinding from './PreferenceKeybinding';
910

1011
const Container = styled.div`
1112
display: flex;
@@ -19,7 +20,7 @@ type Props = {
1920
value: any,
2021
setValue: (value: any) => any,
2122
tooltip: ?string,
22-
type: 'boolean' | 'number' | 'string',
23+
type: 'boolean' | 'number' | 'string' | 'keybinding',
2324
options: ?Array<string>,
2425
};
2526

@@ -59,6 +60,17 @@ export default class Preference extends React.Component {
5960
);
6061
}
6162

63+
if (type === 'keybinding') {
64+
return (
65+
<PreferenceKeybinding
66+
{...this.props}
67+
options={this.props.options}
68+
setValue={this.props.setValue}
69+
value={value}
70+
/>
71+
);
72+
}
73+
6274
return (
6375
<PreferenceNumber
6476
{...this.props}

packages/app/src/app/components/sandbox/CodeEditor/Monaco.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,16 @@ export default class CodeEditor extends React.Component<Props, State> {
728728
this.handleSaveCode();
729729
}
730730
);
731+
732+
const quickCommandAction = this.editor.getAction(
733+
'editor.action.quickCommand'
734+
);
735+
this.editor.addCommand(
736+
this.monaco.KeyMod.CtrlCmd | // eslint-disable-line no-bitwise
737+
this.monaco.KeyMod.Shift | // eslint-disable-line no-bitwise
738+
this.monaco.KeyCode.KEY_P, // eslint-disable-line no-bitwise
739+
quickCommandAction._run // eslint-disable-line no-underscore-dangle
740+
);
731741
};
732742

733743
disposeModules = (modules: Array<Module>) => {

packages/app/src/app/components/sandbox/Preview/DevTools/index.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22
import React from 'react';
33
import styled from 'styled-components';
44
import { TweenMax, Elastic } from 'gsap';
5-
6-
import Tooltip from 'common/components/Tooltip';
7-
5+
import store from 'store/dist/store.modern';
86
import MinimizeIcon from 'react-icons/lib/fa/angle-up';
97

10-
import store from 'store/dist/store.modern';
8+
import Tooltip from 'common/components/Tooltip';
119

1210
import Unread from './Unread';
13-
1411
import console from './Console';
1512

1613
const Container = styled.div`
@@ -80,6 +77,8 @@ type Props = {
8077
sandboxId: string,
8178
zenMode: boolean,
8279
shouldExpandDevTools: ?boolean,
80+
devToolsOpen: ?boolean,
81+
setDevToolsOpen: ?(open: boolean) => void,
8382
};
8483
type State = {
8584
status: { [title: string]: ?Status },
@@ -135,6 +134,19 @@ export default class DevTools extends React.PureComponent<Props, State> {
135134
}
136135
}
137136

137+
componentDidUpdate(prevProps: Props, prevState: State) {
138+
if (
139+
this.props.devToolsOpen !== prevProps.devToolsOpen &&
140+
prevState.hidden === this.state.hidden
141+
) {
142+
if (this.props.devToolsOpen === true && this.state.hidden) {
143+
this.openDevTools();
144+
} else if (this.props.devToolsOpen === false && !this.state.hidden) {
145+
this.hideDevTools();
146+
}
147+
}
148+
}
149+
138150
componentDidMount() {
139151
document.addEventListener('mouseup', this.handleMouseUp, false);
140152
document.addEventListener('mousemove', this.handleMouseMove, false);
@@ -164,7 +176,12 @@ export default class DevTools extends React.PureComponent<Props, State> {
164176
});
165177
}
166178

167-
return this.setState({ hidden });
179+
return this.setState({ hidden }, () => {
180+
if (this.props.setDevToolsOpen) {
181+
const { setDevToolsOpen } = this.props;
182+
setTimeout(() => setDevToolsOpen(!this.state.hidden), 100);
183+
}
184+
});
168185
};
169186

170187
updateStatus = (title: string) => (

packages/app/src/app/components/sandbox/Preview/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ type Props = {
5454
inactive: ?boolean,
5555
shouldExpandDevTools: ?boolean,
5656
entry: string,
57+
devToolsOpen: ?boolean,
58+
setDevToolsOpen: ?(open: boolean) => void,
5759
};
5860

5961
type State = {
@@ -435,6 +437,8 @@ export default class Preview extends React.PureComponent<Props, State> {
435437
sandboxId={sandboxId}
436438
shouldExpandDevTools={shouldExpandDevTools}
437439
zenMode={this.props.preferences.zenMode}
440+
devToolsOpen={this.props.devToolsOpen}
441+
setDevToolsOpen={this.props.setDevToolsOpen}
438442
/>
439443
</Container>
440444
);

0 commit comments

Comments
 (0)