Skip to content

Commit 425818a

Browse files
committed
[processing#3444] Add keyboard shortcut update functionality
1 parent 5b7b8d8 commit 425818a

File tree

5 files changed

+277
-134
lines changed

5 files changed

+277
-134
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { useContext } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { createContext, useState } from 'react';
4+
5+
export const EditorKeyMapsContext = createContext();
6+
7+
export function EditorKeyMapProvider({ children }) {
8+
const [keyMaps, setKeyMaps] = useState({ tidy: 'Shift-Ctrl-F' });
9+
10+
const updateKeyMap = (key, value) => {
11+
if (key in keyMaps) {
12+
setKeyMaps((prevKeyMaps) => ({
13+
...prevKeyMaps,
14+
[key]: value
15+
}));
16+
}
17+
};
18+
19+
return (
20+
<EditorKeyMapsContext.Provider value={{ keyMaps, updateKeyMap }}>
21+
{children}
22+
</EditorKeyMapsContext.Provider>
23+
);
24+
}
25+
26+
EditorKeyMapProvider.propTypes = {
27+
children: PropTypes.node.isRequired
28+
};
29+
30+
export const useEditorKeyMap = () => {
31+
const context = useContext(EditorKeyMapsContext);
32+
if (!context) {
33+
throw new Error(
34+
'useEditorKeyMap must be used within a EditorKeyMapProvider'
35+
);
36+
}
37+
return context;
38+
};

client/modules/IDE/components/Editor/index.jsx

+51-30
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import UnsavedChangesIndicator from '../UnsavedChangesIndicator';
7272
import { EditorContainer, EditorHolder } from './MobileEditor';
7373
import { FolderIcon } from '../../../../common/icons';
7474
import IconButton from '../../../../common/IconButton';
75+
import { EditorKeyMapsContext } from './contexts';
7576

7677
emmet(CodeMirror);
7778

@@ -104,6 +105,7 @@ class Editor extends React.Component {
104105
this.showFind = this.showFind.bind(this);
105106
this.showReplace = this.showReplace.bind(this);
106107
this.getContent = this.getContent.bind(this);
108+
this.updateKeyMaps = this.updateKeyMaps.bind(this);
107109
}
108110

109111
componentDidMount() {
@@ -153,36 +155,9 @@ class Editor extends React.Component {
153155

154156
delete this._cm.options.lint.options.errors;
155157

156-
const replaceCommand =
157-
metaKey === 'Ctrl' ? `${metaKey}-H` : `${metaKey}-Option-F`;
158-
this._cm.setOption('extraKeys', {
159-
Tab: (cm) => {
160-
if (!cm.execCommand('emmetExpandAbbreviation')) return;
161-
// might need to specify and indent more?
162-
const selection = cm.doc.getSelection();
163-
if (selection.length > 0) {
164-
cm.execCommand('indentMore');
165-
} else {
166-
cm.replaceSelection(' '.repeat(INDENTATION_AMOUNT));
167-
}
168-
},
169-
Enter: 'emmetInsertLineBreak',
170-
Esc: 'emmetResetAbbreviation',
171-
[`Shift-Tab`]: false,
172-
[`${metaKey}-Enter`]: () => null,
173-
[`Shift-${metaKey}-Enter`]: () => null,
174-
[`${metaKey}-F`]: 'findPersistent',
175-
[`Shift-${metaKey}-F`]: this.tidyCode,
176-
[`${metaKey}-G`]: 'findPersistentNext',
177-
[`Shift-${metaKey}-G`]: 'findPersistentPrev',
178-
[replaceCommand]: 'replace',
179-
// Cassie Tarakajian: If you don't set a default color, then when you
180-
// choose a color, it deletes characters inline. This is a
181-
// hack to prevent that.
182-
[`${metaKey}-K`]: (cm, event) =>
183-
cm.state.colorpicker.popup_color_picker({ length: 0 }),
184-
[`${metaKey}-.`]: 'toggleComment' // Note: most adblockers use the shortcut ctrl+.
185-
});
158+
const { keyMaps } = this.context;
159+
160+
this.updateKeyMaps(keyMaps);
186161

187162
this.initializeDocuments(this.props.files);
188163
this._cm.swapDoc(this._docs[this.props.file.id]);
@@ -236,6 +211,16 @@ class Editor extends React.Component {
236211
}
237212

238213
componentDidUpdate(prevProps) {
214+
const currentKeyMaps = this.context?.keyMaps;
215+
const prevKeyMaps = this.prevKeyMapsRef?.current;
216+
217+
if (
218+
prevKeyMaps &&
219+
JSON.stringify(currentKeyMaps) !== JSON.stringify(prevKeyMaps)
220+
) {
221+
this.updateKeyMaps(currentKeyMaps);
222+
}
223+
this.prevKeyMapsRef = { current: currentKeyMaps };
239224
if (this.props.file.id !== prevProps.file.id) {
240225
const fileMode = this.getFileMode(this.props.file.name);
241226
if (fileMode === 'javascript') {
@@ -515,6 +500,42 @@ class Editor extends React.Component {
515500
});
516501
}
517502

503+
updateKeyMaps(keyMaps) {
504+
const replaceCommand =
505+
metaKey === 'Ctrl' ? `${metaKey}-H` : `${metaKey}-Option-F`;
506+
507+
this._cm.setOption('extraKeys', {
508+
Tab: (cm) => {
509+
if (!cm.execCommand('emmetExpandAbbreviation')) return;
510+
// might need to specify and indent more?
511+
const selection = cm.doc.getSelection();
512+
if (selection.length > 0) {
513+
cm.execCommand('indentMore');
514+
} else {
515+
cm.replaceSelection(' '.repeat(INDENTATION_AMOUNT));
516+
}
517+
},
518+
Enter: 'emmetInsertLineBreak',
519+
Esc: 'emmetResetAbbreviation',
520+
[`Shift-Tab`]: false,
521+
[`${metaKey}-Enter`]: () => null,
522+
[`Shift-${metaKey}-Enter`]: () => null,
523+
[`${metaKey}-F`]: 'findPersistent',
524+
[`${keyMaps.tidy}`]: this.tidyCode,
525+
[`${metaKey}-G`]: 'findPersistentNext',
526+
[`Shift-${metaKey}-G`]: 'findPersistentPrev',
527+
[replaceCommand]: 'replace',
528+
// Cassie Tarakajian: If you don't set a default color, then when you
529+
// choose a color, it deletes characters inline. This is a
530+
// hack to prevent that.
531+
[`${metaKey}-K`]: (cm, event) =>
532+
cm.state.colorpicker.popup_color_picker({ length: 0 }),
533+
[`${metaKey}-.`]: 'toggleComment' // Note: most adblockers use the shortcut ctrl+.
534+
});
535+
}
536+
537+
static contextType = EditorKeyMapsContext;
538+
518539
render() {
519540
const editorSectionClass = classNames({
520541
editor: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, { useRef, useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useEditorKeyMap } from './Editor/contexts';
4+
5+
function KeyboardShortcutItem({ shortcut, desc }) {
6+
const [edit, setEdit] = useState(false);
7+
const pressedKeyCombination = useRef({});
8+
const inputRef = useRef(null);
9+
const { updateKeyMap } = useEditorKeyMap();
10+
11+
const handleEdit = (state) => {
12+
setEdit(state);
13+
if (state) {
14+
inputRef.current.focus();
15+
} else {
16+
inputRef.current.blur();
17+
updateKeyMap('tidy', inputRef.current.innerText);
18+
}
19+
};
20+
21+
return (
22+
<li className="keyboard-shortcut-item">
23+
<button type="button" title="edit" onClick={() => handleEdit(!edit)}>
24+
&#x270E;
25+
</button>
26+
<span
27+
className="keyboard-shortcut__command"
28+
role="textbox"
29+
ref={inputRef}
30+
tabIndex={0}
31+
contentEditable={edit}
32+
suppressContentEditableWarning
33+
onKeyDown={(event) => {
34+
if (!edit) return;
35+
36+
event.preventDefault();
37+
event.stopPropagation();
38+
let { key } = event;
39+
if (key === 'Control') {
40+
key = 'Ctrl';
41+
}
42+
if (key === ' ') {
43+
key = 'Space';
44+
}
45+
46+
pressedKeyCombination.current[key] = true;
47+
48+
event.currentTarget.innerText = Object.keys(
49+
pressedKeyCombination.current
50+
).join('-');
51+
}}
52+
onKeyUp={(event) => {
53+
if (!edit) return;
54+
event.preventDefault();
55+
event.stopPropagation();
56+
let { key } = event;
57+
if (key === 'Control') {
58+
key = 'Ctrl';
59+
}
60+
if (key === ' ') {
61+
key = 'Space';
62+
}
63+
64+
delete pressedKeyCombination.current[key];
65+
}}
66+
>
67+
{shortcut}
68+
</span>
69+
<span>{desc}</span>
70+
</li>
71+
);
72+
}
73+
74+
KeyboardShortcutItem.propTypes = {
75+
shortcut: PropTypes.string.isRequired,
76+
desc: PropTypes.string.isRequired
77+
};
78+
79+
export default KeyboardShortcutItem;

client/modules/IDE/components/KeyboardShortcutModal.jsx

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import React from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { metaKeyName, metaKey } from '../../../utils/metaKey';
4+
import KeyboardShortcutItem from './KeyboardShortcutItem';
5+
import { useEditorKeyMap } from './Editor/contexts';
46

57
function KeyboardShortcutModal() {
68
const { t } = useTranslation();
9+
const { keyMaps } = useEditorKeyMap();
10+
711
const replaceCommand =
812
metaKey === 'Ctrl' ? `${metaKeyName} + H` : `${metaKeyName} + ⌥ + F`;
913
const newFileCommand =
@@ -25,12 +29,10 @@ function KeyboardShortcutModal() {
2529
.
2630
</p>
2731
<ul className="keyboard-shortcuts__list">
28-
<li className="keyboard-shortcut-item">
29-
<span className="keyboard-shortcut__command">
30-
{metaKeyName} + Shift + F
31-
</span>
32-
<span>{t('KeyboardShortcuts.CodeEditing.Tidy')}</span>
33-
</li>
32+
<KeyboardShortcutItem
33+
shortcut={keyMaps.tidy}
34+
desc={t('KeyboardShortcuts.CodeEditing.Tidy')}
35+
/>
3436
<li className="keyboard-shortcut-item">
3537
<span className="keyboard-shortcut__command">{metaKeyName} + F</span>
3638
<span>{t('KeyboardShortcuts.CodeEditing.FindText')}</span>

0 commit comments

Comments
 (0)