Skip to content

Commit 9597861

Browse files
committed
Move keydown handling out of IDEView
1 parent fc1e4b4 commit 9597861

File tree

8 files changed

+168
-201
lines changed

8 files changed

+168
-201
lines changed

client/components/Nav.jsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
setAllAccessibleOutput,
1414
setLanguage
1515
} from '../modules/IDE/actions/preferences';
16+
import { DocumentKeyDown } from '../modules/IDE/hooks/useKeyDownHandlers';
1617
import { logoutUser } from '../modules/User/actions';
1718

1819
import getConfig from '../utils/getConfig';
@@ -63,30 +64,20 @@ class Nav extends React.PureComponent {
6364
this.toggleDropdownForLang = this.toggleDropdown.bind(this, 'lang');
6465
this.handleFocusForLang = this.handleFocus.bind(this, 'lang');
6566
this.handleLangSelection = this.handleLangSelection.bind(this);
66-
67-
this.closeDropDown = this.closeDropDown.bind(this);
6867
}
6968

7069
componentDidMount() {
7170
document.addEventListener('mousedown', this.handleClick, false);
72-
document.addEventListener('keydown', this.closeDropDown, false);
7371
}
7472
componentWillUnmount() {
7573
document.removeEventListener('mousedown', this.handleClick, false);
76-
document.removeEventListener('keydown', this.closeDropDown, false);
7774
}
7875
setDropdown(dropdown) {
7976
this.setState({
8077
dropdownOpen: dropdown
8178
});
8279
}
8380

84-
closeDropDown(e) {
85-
if (e.keyCode === 27) {
86-
this.setDropdown('none');
87-
}
88-
}
89-
9081
handleClick(e) {
9182
if (!this.node) {
9283
return;
@@ -904,6 +895,11 @@ class Nav extends React.PureComponent {
904895
{this.renderLeftLayout(navDropdownState)}
905896
{this.renderUserMenu(navDropdownState)}
906897
</nav>
898+
<DocumentKeyDown
899+
handlers={{
900+
escape: () => this.setDropdown('none')
901+
}}
902+
/>
907903
</header>
908904
);
909905
}

client/modules/App/components/Overlay.jsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@ import { browserHistory } from 'react-router';
44
import { withTranslation } from 'react-i18next';
55

66
import ExitIcon from '../../../images/exit.svg';
7+
import { DocumentKeyDown } from '../../IDE/hooks/useKeyDownHandlers';
78

89
class Overlay extends React.Component {
910
constructor(props) {
1011
super(props);
1112
this.close = this.close.bind(this);
1213
this.handleClick = this.handleClick.bind(this);
1314
this.handleClickOutside = this.handleClickOutside.bind(this);
14-
this.keyPressHandle = this.keyPressHandle.bind(this);
1515
}
1616

1717
componentWillMount() {
1818
document.addEventListener('mousedown', this.handleClick, false);
19-
document.addEventListener('keydown', this.keyPressHandle);
2019
}
2120

2221
componentDidMount() {
@@ -25,7 +24,6 @@ class Overlay extends React.Component {
2524

2625
componentWillUnmount() {
2726
document.removeEventListener('mousedown', this.handleClick, false);
28-
document.removeEventListener('keydown', this.keyPressHandle);
2927
}
3028

3129
handleClick(e) {
@@ -40,14 +38,6 @@ class Overlay extends React.Component {
4038
this.close();
4139
}
4240

43-
keyPressHandle(e) {
44-
// escape key code = 27.
45-
// So here we are checking if the key pressed was Escape key.
46-
if (e.keyCode === 27) {
47-
this.close();
48-
}
49-
}
50-
5141
close() {
5242
// Only close if it is the last (and therefore the topmost overlay)
5343
const overlays = document.getElementsByClassName('overlay');
@@ -90,6 +80,7 @@ class Overlay extends React.Component {
9080
</div>
9181
</header>
9282
{children}
83+
<DocumentKeyDown handlers={{ escape: () => this.close() }} />
9384
</section>
9485
</div>
9586
</div>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import PropTypes from 'prop-types';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
import { updateFileContent } from '../actions/files';
4+
import {
5+
collapseConsole,
6+
collapseSidebar,
7+
expandConsole,
8+
expandSidebar,
9+
showErrorModal,
10+
startSketch,
11+
stopSketch
12+
} from '../actions/ide';
13+
import { setAllAccessibleOutput } from '../actions/preferences';
14+
import { cloneProject, saveProject } from '../actions/project';
15+
import useKeyDownHandlers from '../hooks/useKeyDownHandlers';
16+
import {
17+
getAuthenticated,
18+
getIsUserOwner,
19+
getSketchOwner
20+
} from '../selectors/users';
21+
22+
export const useIDEKeyHandlers = ({ getContent }) => {
23+
const dispatch = useDispatch();
24+
25+
const sidebarIsExpanded = useSelector((state) => state.ide.sidebarIsExpanded);
26+
const consoleIsExpanded = useSelector((state) => state.ide.consoleIsExpanded);
27+
28+
const isUserOwner = useSelector(getIsUserOwner);
29+
const isAuthenticated = useSelector(getAuthenticated);
30+
const sketchOwner = useSelector(getSketchOwner);
31+
32+
const syncFileContent = () => {
33+
const file = getContent();
34+
dispatch(updateFileContent(file.id, file.content));
35+
};
36+
37+
useKeyDownHandlers({
38+
'ctrl-s': (e) => {
39+
e.preventDefault();
40+
e.stopPropagation();
41+
if (isUserOwner || (isAuthenticated && !sketchOwner)) {
42+
dispatch(saveProject(getContent()));
43+
} else if (isAuthenticated) {
44+
dispatch(cloneProject());
45+
} else {
46+
dispatch(showErrorModal('forceAuthentication'));
47+
}
48+
},
49+
'ctrl-shift-enter': (e) => {
50+
e.preventDefault();
51+
e.stopPropagation();
52+
dispatch(stopSketch());
53+
},
54+
'ctrl-enter': (e) => {
55+
e.preventDefault();
56+
e.stopPropagation();
57+
syncFileContent();
58+
dispatch(startSketch());
59+
},
60+
'ctrl-shift-1': (e) => {
61+
e.preventDefault();
62+
dispatch(setAllAccessibleOutput(true));
63+
},
64+
'ctrl-shift-2': (e) => {
65+
e.preventDefault();
66+
dispatch(setAllAccessibleOutput(false));
67+
},
68+
'ctrl-b': (e) => {
69+
e.preventDefault();
70+
dispatch(
71+
// TODO: create actions 'toggleConsole', 'toggleSidebar', etc.
72+
sidebarIsExpanded ? collapseSidebar() : expandSidebar()
73+
);
74+
},
75+
'ctrl-`': (e) => {
76+
e.preventDefault();
77+
dispatch(consoleIsExpanded ? collapseConsole() : expandConsole());
78+
}
79+
});
80+
};
81+
82+
const IDEKeyHandlers = ({ getContent }) => {
83+
useIDEKeyHandlers({ getContent });
84+
return null;
85+
};
86+
87+
// Most actions can be accessed via redux, but those involving the cmController
88+
// must be provided via props.
89+
IDEKeyHandlers.propTypes = {
90+
getContent: PropTypes.func.isRequired
91+
};
92+
93+
export default IDEKeyHandlers;

client/modules/IDE/components/Modal.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import classNames from 'classnames';
22
import PropTypes from 'prop-types';
33
import React, { useEffect, useRef } from 'react';
44
import ExitIcon from '../../../images/exit.svg';
5+
import useKeyDownHandlers from '../hooks/useKeyDownHandlers';
56

67
// Common logic from NewFolderModal, NewFileModal, UploadFileModal
78

@@ -30,6 +31,8 @@ const Modal = ({
3031
};
3132
}, []);
3233

34+
useKeyDownHandlers({ escape: onClose });
35+
3336
return (
3437
<section className="modal" ref={modalRef}>
3538
<div className={classNames('modal-content', contentClassName)}>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import mapKeys from 'lodash/mapKeys';
2+
import { useCallback, useEffect, useRef } from 'react';
3+
4+
/**
5+
* Attaches keydown handlers to the global document.
6+
*
7+
* Handles Mac/PC switching of Ctrl to Cmd.
8+
*
9+
* @param {Record<string, (e: KeyboardEvent) => void>} keyHandlers - an object
10+
* which maps from the key to its event handler. The object keys are a combination
11+
* of the key and prefixes `ctrl-` `shift-` (ie. 'ctrl-f', 'ctrl-shift-f')
12+
* and the values are the function to call when that specific key is pressed.
13+
*/
14+
export default function useKeyDownHandlers(keyHandlers) {
15+
/**
16+
* Instead of memoizing the handlers, use a ref and call the current
17+
* handler at the time of the event.
18+
*/
19+
const handlers = useRef(keyHandlers);
20+
21+
useEffect(() => {
22+
handlers.current = mapKeys(keyHandlers, (value, key) => key.toLowerCase());
23+
}, [keyHandlers]);
24+
25+
/**
26+
* Will call all matching handlers, starting with the most specific: 'ctrl-shift-f' => 'ctrl-f' => 'f'.
27+
* Can use e.stopPropagation() to prevent subsequent handlers.
28+
* @type {(function(KeyboardEvent): void)}
29+
*/
30+
const handleEvent = useCallback((e) => {
31+
const isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
32+
const isCtrl = isMac ? e.metaKey && this.isMac : e.ctrlKey;
33+
if (e.shiftKey && isCtrl) {
34+
handlers.current[`ctrl-shift-${e.key.toLowerCase()}`]?.(e);
35+
}
36+
if (isCtrl) {
37+
handlers.current[`ctrl-${e.key.toLowerCase()}`]?.(e);
38+
}
39+
handlers.current[e.key.toLowerCase()]?.(e);
40+
}, []);
41+
42+
useEffect(() => {
43+
document.addEventListener('keydown', handleEvent);
44+
45+
return () => document.removeEventListener('keydown', handleEvent);
46+
}, [handleEvent]);
47+
}
48+
49+
/**
50+
* Component version can be used in class components where hooks can't be used.
51+
*/
52+
export const DocumentKeyDown = ({ handlers }) => {
53+
useKeyDownHandlers(handlers);
54+
return null;
55+
};

0 commit comments

Comments
 (0)