Skip to content

Commit 86f47df

Browse files
committed
Add initial keymap configuration
Add shortcuts for desktop and web app - Sidenav toggle - Global Search toggle - Preferences toggle Comment out keymaps not implemented
1 parent fc801b6 commit 86f47df

File tree

18 files changed

+1145
-56
lines changed

18 files changed

+1145
-56
lines changed

src/cloud/components/Application.tsx

+58-15
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
import { isActiveElementAnInput, InputableDomElement } from '../lib/dom'
1010
import { useEffectOnce } from 'react-use'
1111
import { useSettings } from '../lib/stores/settings'
12-
import { isPageSearchShortcut, isSidebarToggleShortcut } from '../lib/shortcuts'
1312
import { useSearch } from '../lib/stores/search'
1413
import AnnouncementAlert from './AnnouncementAlert'
1514
import {
@@ -20,6 +19,8 @@ import {
2019
newDocEventEmitter,
2120
switchSpaceEventEmitter,
2221
SwitchSpaceEventDetails,
22+
togglePreviewModeEventEmitter,
23+
toggleSplitEditModeEventEmitter,
2324
} from '../lib/utils/events'
2425
import { usePathnameChangeEffect, useRouter } from '../lib/router'
2526
import { useNav } from '../lib/stores/nav'
@@ -76,6 +77,7 @@ import {
7677
} from './molecules/PageSearch/InPageSearchPortal'
7778
import SidebarToggleButton from './SidebarToggleButton'
7879
import { getTeamURL } from '../lib/utils/patterns'
80+
import { compareEventKeyWithKeymap } from '../../lib/keymap'
7981

8082
interface ApplicationProps {
8183
className?: string
@@ -140,7 +142,9 @@ const Application = ({
140142

141143
useEffect(() => {
142144
const handler = () => {
143-
setShowFuzzyNavigation((prev) => !prev)
145+
if (usingElectron) {
146+
setShowFuzzyNavigation((prev) => !prev)
147+
}
144148
}
145149
searchEventEmitter.listen(handler)
146150
return () => {
@@ -229,11 +233,47 @@ const Application = ({
229233
return
230234
}
231235

232-
if (isSidebarToggleShortcut(event)) {
233-
preventKeyboardEventPropagation(event)
234-
setPreferences((prev) => {
235-
return { sidebarIsHidden: !prev.sidebarIsHidden }
236-
})
236+
const keymap = preferences['keymap']
237+
if (keymap != null) {
238+
const sidenavToggleShortcut = keymap.get('toggleSideNav')
239+
if (compareEventKeyWithKeymap(sidenavToggleShortcut, event)) {
240+
preventKeyboardEventPropagation(event)
241+
setPreferences((prev) => {
242+
return { sidebarIsHidden: !prev.sidebarIsHidden }
243+
})
244+
}
245+
}
246+
247+
if (!usingElectron && keymap != null) {
248+
const toggleGlobalSearchShortcut = keymap.get('toggleGlobalSearch')
249+
if (compareEventKeyWithKeymap(toggleGlobalSearchShortcut, event)) {
250+
preventKeyboardEventPropagation(event)
251+
searchEventEmitter.dispatch()
252+
}
253+
}
254+
255+
if (!usingElectron && keymap != null) {
256+
const openPreferencesShortcut = keymap.get('openPreferences')
257+
if (compareEventKeyWithKeymap(openPreferencesShortcut, event)) {
258+
preventKeyboardEventPropagation(event)
259+
openSettingsTab('preferences')
260+
}
261+
}
262+
263+
if (!usingElectron && keymap != null) {
264+
const togglePreviewModeShortcut = keymap.get('togglePreviewMode')
265+
if (compareEventKeyWithKeymap(togglePreviewModeShortcut, event)) {
266+
preventKeyboardEventPropagation(event)
267+
togglePreviewModeEventEmitter.dispatch()
268+
}
269+
}
270+
271+
if (!usingElectron && keymap != null) {
272+
const toggleSplitEditModeShortcut = keymap.get('toggleSplitEditMode')
273+
if (compareEventKeyWithKeymap(toggleSplitEditModeShortcut, event)) {
274+
preventKeyboardEventPropagation(event)
275+
toggleSplitEditModeEventEmitter.dispatch()
276+
}
237277
}
238278

239279
if (isSingleKeyEvent(event, 'escape') && isActiveElementAnInput()) {
@@ -244,17 +284,20 @@ const Application = ({
244284
;(document.activeElement as InputableDomElement).blur()
245285
}
246286

247-
if (usingElectron && isPageSearchShortcut(event)) {
248-
preventKeyboardEventPropagation(event)
249-
if (showInPageSearch) {
250-
setShowInPageSearch(false)
251-
setShowInPageSearch(true)
252-
} else {
253-
setShowInPageSearch(true)
287+
if (usingElectron && keymap != null) {
288+
const inPageSearchShortcut = keymap.get('toggleInPageSearch')
289+
if (compareEventKeyWithKeymap(inPageSearchShortcut, event)) {
290+
preventKeyboardEventPropagation(event)
291+
if (showInPageSearch) {
292+
setShowInPageSearch(false)
293+
setShowInPageSearch(true)
294+
} else {
295+
setShowInPageSearch(true)
296+
}
254297
}
255298
}
256299
},
257-
[team, setPreferences, showInPageSearch]
300+
[team, preferences, setPreferences, openSettingsTab, showInPageSearch]
258301
)
259302
useGlobalKeyDownHandler(overrideBrowserCtrlsHandler)
260303

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import React, {
2+
KeyboardEventHandler,
3+
useCallback,
4+
useMemo,
5+
useRef,
6+
useState,
7+
} from 'react'
8+
import {
9+
getGenericShortcutString,
10+
KeymapItemEditableProps,
11+
} from '../../../lib/keymap'
12+
import Button from '../../../design/components/atoms/Button'
13+
import styled from '../../../design/lib/styled'
14+
import { inputStyle } from '../../../design/lib/styled/styleFunctions'
15+
import cc from 'classcat'
16+
import { useToast } from '../../../design/lib/stores/toast'
17+
18+
const invalidShortcutInputs = [' ']
19+
const rejectedShortcutInputs = [' ', 'control', 'alt', 'shift', 'meta']
20+
21+
interface KeymapItemSectionProps {
22+
keymapKey: string
23+
currentKeymapItem?: KeymapItemEditableProps
24+
updateKeymap: (
25+
key: string,
26+
shortcutFirst: KeymapItemEditableProps,
27+
shortcutSecond?: KeymapItemEditableProps
28+
) => Promise<void>
29+
removeKeymap: (key: string) => void
30+
description: string
31+
}
32+
33+
const KeymapItemSection = ({
34+
keymapKey,
35+
currentKeymapItem,
36+
updateKeymap,
37+
removeKeymap,
38+
description,
39+
}: KeymapItemSectionProps) => {
40+
const [inputError, setInputError] = useState<boolean>(false)
41+
const [shortcutInputValue, setShortcutInputValue] = useState<string>('')
42+
const [changingShortcut, setChangingShortcut] = useState<boolean>(false)
43+
const [
44+
currentShortcut,
45+
setCurrentShortcut,
46+
] = useState<KeymapItemEditableProps | null>(
47+
currentKeymapItem != null ? currentKeymapItem : null
48+
)
49+
const [
50+
previousShortcut,
51+
setPreviousShortcut,
52+
] = useState<KeymapItemEditableProps | null>(null)
53+
const shortcutInputRef = useRef<HTMLInputElement>(null)
54+
55+
const { pushMessage } = useToast()
56+
57+
const fetchInputShortcuts: KeyboardEventHandler<HTMLInputElement> = (
58+
event
59+
) => {
60+
event.stopPropagation()
61+
event.preventDefault()
62+
if (invalidShortcutInputs.includes(event.key.toLowerCase())) {
63+
setInputError(true)
64+
return
65+
}
66+
67+
setInputError(false)
68+
69+
const shortcut: KeymapItemEditableProps = {
70+
key: event.key.toUpperCase(),
71+
keycode: event.keyCode,
72+
modifiers: {
73+
ctrl: event.ctrlKey,
74+
alt: event.altKey,
75+
shift: event.shiftKey,
76+
meta: event.metaKey,
77+
},
78+
}
79+
setCurrentShortcut(shortcut)
80+
setShortcutInputValue(getGenericShortcutString(shortcut))
81+
}
82+
83+
const applyKeymap = useCallback(() => {
84+
if (currentShortcut == null) {
85+
return
86+
}
87+
if (rejectedShortcutInputs.includes(currentShortcut.key.toLowerCase())) {
88+
setInputError(true)
89+
if (shortcutInputRef.current != null) {
90+
shortcutInputRef.current.focus()
91+
}
92+
return
93+
}
94+
95+
updateKeymap(keymapKey, currentShortcut, undefined)
96+
.then(() => {
97+
setChangingShortcut(false)
98+
setInputError(false)
99+
})
100+
.catch(() => {
101+
pushMessage({
102+
title: 'Keymap assignment failed',
103+
description: 'Cannot assign to already assigned shortcut',
104+
})
105+
setInputError(true)
106+
})
107+
}, [currentShortcut, keymapKey, updateKeymap, pushMessage])
108+
109+
const toggleChangingShortcut = useCallback(() => {
110+
if (changingShortcut) {
111+
applyKeymap()
112+
} else {
113+
setChangingShortcut(true)
114+
setPreviousShortcut(currentShortcut)
115+
if (currentShortcut != null) {
116+
setShortcutInputValue(getGenericShortcutString(currentShortcut))
117+
}
118+
}
119+
}, [applyKeymap, currentShortcut, changingShortcut])
120+
121+
const handleCancelKeymapChange = useCallback(() => {
122+
setCurrentShortcut(previousShortcut)
123+
setChangingShortcut(false)
124+
setShortcutInputValue('')
125+
setInputError(false)
126+
}, [previousShortcut])
127+
128+
const handleRemoveKeymap = useCallback(() => {
129+
setCurrentShortcut(null)
130+
setPreviousShortcut(null)
131+
setShortcutInputValue('')
132+
removeKeymap(keymapKey)
133+
}, [keymapKey, removeKeymap])
134+
135+
const shortcutString = useMemo(() => {
136+
return currentShortcut != null && currentKeymapItem != null
137+
? getGenericShortcutString(currentKeymapItem)
138+
: ''
139+
}, [currentKeymapItem, currentShortcut])
140+
return (
141+
<KeymapItemSectionContainer>
142+
<div>{description}</div>
143+
<KeymapItemInputSection>
144+
{currentShortcut != null && currentKeymapItem != null && (
145+
<ShortcutItemStyle>{shortcutString}</ShortcutItemStyle>
146+
)}
147+
{changingShortcut && (
148+
<StyledInput
149+
className={cc([inputError && 'error'])}
150+
placeholder={'Press key'}
151+
autoFocus={true}
152+
ref={shortcutInputRef}
153+
value={shortcutInputValue}
154+
onChange={() => undefined}
155+
onKeyDown={fetchInputShortcuts}
156+
/>
157+
)}
158+
<Button variant={'primary'} onClick={toggleChangingShortcut}>
159+
{currentShortcut == null
160+
? 'Assign'
161+
: changingShortcut
162+
? 'Apply'
163+
: 'Change'}
164+
</Button>
165+
{changingShortcut && (
166+
<Button onClick={handleCancelKeymapChange}>Cancel</Button>
167+
)}
168+
169+
{currentShortcut != null && !changingShortcut && (
170+
<Button onClick={handleRemoveKeymap}>Un-assign</Button>
171+
)}
172+
</KeymapItemInputSection>
173+
</KeymapItemSectionContainer>
174+
)
175+
}
176+
177+
const ShortcutItemStyle = styled.div`
178+
min-width: 88px;
179+
max-width: 120px;
180+
height: 32px;
181+
font-size: 15px;
182+
display: flex;
183+
align-items: center;
184+
justify-content: center;
185+
186+
background-color: ${({ theme }) => theme.colors.background.tertiary};
187+
color: ${({ theme }) => theme.colors.text.primary};
188+
189+
border: 1px solid ${({ theme }) => theme.colors.border.main};
190+
border-radius: 4px;
191+
`
192+
193+
const StyledInput = styled.input`
194+
${inputStyle};
195+
max-width: 120px;
196+
min-width: 110px;
197+
height: 1.3em;
198+
199+
&.error {
200+
border: 1px solid red;
201+
}
202+
`
203+
204+
const KeymapItemSectionContainer = styled.div`
205+
display: grid;
206+
grid-template-columns: 45% minmax(55%, 400px);
207+
`
208+
209+
const KeymapItemInputSection = styled.div`
210+
display: grid;
211+
grid-auto-flow: column;
212+
align-items: center;
213+
214+
max-width: 380px;
215+
justify-items: left;
216+
217+
margin-right: auto;
218+
219+
column-gap: 1em;
220+
`
221+
222+
export default KeymapItemSection

0 commit comments

Comments
 (0)