@@ -42,16 +42,18 @@ import * as detect from 'enso-common/src/detect'
42
42
43
43
import * as appUtils from '#/appUtils'
44
44
45
+ import * as inputBindingsModule from '#/configurations/inputBindings'
46
+
45
47
import * as navigateHooks from '#/hooks/navigateHooks'
46
48
47
49
import AuthProvider , * as authProvider from '#/providers/AuthProvider'
48
50
import BackendProvider from '#/providers/BackendProvider'
49
- import LocalStorageProvider from '#/providers/LocalStorageProvider'
51
+ import InputBindingsProvider from '#/providers/InputBindingsProvider'
52
+ import LocalStorageProvider , * as localStorageProvider from '#/providers/LocalStorageProvider'
50
53
import LoggerProvider from '#/providers/LoggerProvider'
51
54
import type * as loggerProvider from '#/providers/LoggerProvider'
52
55
import ModalProvider from '#/providers/ModalProvider'
53
56
import SessionProvider from '#/providers/SessionProvider'
54
- import ShortcutManagerProvider from '#/providers/ShortcutManagerProvider'
55
57
56
58
import ConfirmRegistration from '#/pages/authentication/ConfirmRegistration'
57
59
import EnterOfflineMode from '#/pages/authentication/EnterOfflineMode'
@@ -66,10 +68,39 @@ import Subscribe from '#/pages/subscribe/Subscribe'
66
68
import type Backend from '#/services/Backend'
67
69
import LocalBackend from '#/services/LocalBackend'
68
70
69
- import ShortcutManager , * as shortcutManagerModule from '#/utilities/ShortcutManager '
71
+ import LocalStorage from '#/utilities/LocalStorage '
70
72
71
73
import * as authServiceModule from '#/authentication/service'
72
74
75
+ // ============================
76
+ // === Global configuration ===
77
+ // ============================
78
+
79
+ declare module '#/utilities/LocalStorage' {
80
+ /** */
81
+ interface LocalStorageData {
82
+ readonly inputBindings : Partial <
83
+ Readonly < Record < inputBindingsModule . DashboardBindingKey , string [ ] > >
84
+ >
85
+ }
86
+ }
87
+
88
+ LocalStorage . registerKey ( 'inputBindings' , {
89
+ tryParse : value =>
90
+ typeof value !== 'object' || value == null
91
+ ? null
92
+ : Object . fromEntries (
93
+ // This is SAFE, as it is a readonly upcast.
94
+ // eslint-disable-next-line no-restricted-syntax
95
+ Object . entries ( value as Readonly < Record < string , unknown > > ) . flatMap ( kv => {
96
+ const [ k , v ] = kv
97
+ return Array . isArray ( v ) && v . every ( ( item ) : item is string => typeof item === 'string' )
98
+ ? [ [ k , v ] ]
99
+ : [ ]
100
+ } )
101
+ ) ,
102
+ } )
103
+
73
104
// ======================
74
105
// === getMainPageUrl ===
75
106
// ======================
@@ -113,8 +144,9 @@ export default function App(props: AppProps) {
113
144
// This is a React component even though it does not contain JSX.
114
145
// eslint-disable-next-line no-restricted-syntax
115
146
const Router = detect . isOnElectron ( ) ? router . MemoryRouter : router . BrowserRouter
116
- /** Note that the `Router` must be the parent of the `AuthProvider`, because the `AuthProvider`
117
- * will redirect the user between the login/register pages and the dashboard. */
147
+ // Both `BackendProvider` and `InputBindingsProvider` depend on `LocalStorageProvider`.
148
+ // Note that the `Router` must be the parent of the `AuthProvider`, because the `AuthProvider`
149
+ // will redirect the user between the login/register pages and the dashboard.
118
150
return (
119
151
< >
120
152
< toastify . ToastContainer
@@ -127,7 +159,9 @@ export default function App(props: AppProps) {
127
159
limit = { 3 }
128
160
/>
129
161
< Router basename = { getMainPageUrl ( ) . pathname } >
130
- < AppRouter { ...props } />
162
+ < LocalStorageProvider >
163
+ < AppRouter { ...props } />
164
+ </ LocalStorageProvider >
131
165
</ Router >
132
166
</ >
133
167
)
@@ -145,32 +179,67 @@ export default function App(props: AppProps) {
145
179
function AppRouter ( props : AppProps ) {
146
180
const { logger, supportsLocalBackend, isAuthenticationDisabled, shouldShowDashboard } = props
147
181
const { onAuthenticated, projectManagerUrl } = props
182
+ const { localStorage } = localStorageProvider . useLocalStorage ( )
148
183
const navigate = navigateHooks . useNavigate ( )
149
184
if ( detect . IS_DEV_MODE ) {
150
185
// @ts -expect-error This is used exclusively for debugging.
151
186
window . navigate = navigate
152
187
}
153
- const [ shortcutManager ] = React . useState ( ( ) => ShortcutManager . createWithDefaults ( ) )
188
+ const [ inputBindingsRaw ] = React . useState ( ( ) => inputBindingsModule . createBindings ( ) )
154
189
React . useEffect ( ( ) => {
155
- const onKeyDown = ( event : KeyboardEvent ) => {
156
- const isTargetEditable =
157
- event . target instanceof HTMLInputElement ||
158
- ( event . target instanceof HTMLElement && event . target . isContentEditable )
159
- const shouldHandleEvent = isTargetEditable
160
- ? ! shortcutManagerModule . isTextInputEvent ( event )
161
- : true
162
- if ( shouldHandleEvent && shortcutManager . handleKeyboardEvent ( event ) ) {
163
- event . preventDefault ( )
164
- // This is required to prevent the event from propagating to the event handler
165
- // that focuses the search input.
166
- event . stopImmediatePropagation ( )
190
+ const savedInputBindings = localStorage . get ( 'inputBindings' )
191
+ for ( const k in savedInputBindings ) {
192
+ // This is UNSAFE, hence the `?? []` below.
193
+ // eslint-disable-next-line no-restricted-syntax
194
+ const bindingKey = k as inputBindingsModule . DashboardBindingKey
195
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
196
+ for ( const oldBinding of inputBindingsRaw . metadata [ bindingKey ] . bindings ?? [ ] ) {
197
+ inputBindingsRaw . delete ( bindingKey , oldBinding )
198
+ }
199
+ for ( const newBinding of savedInputBindings [ bindingKey ] ?? [ ] ) {
200
+ inputBindingsRaw . add ( bindingKey , newBinding )
167
201
}
168
202
}
169
- document . body . addEventListener ( 'keydown' , onKeyDown )
170
- return ( ) => {
171
- document . body . removeEventListener ( 'keydown' , onKeyDown )
203
+ } , [ /* should never change */ localStorage , /* should never change */ inputBindingsRaw ] )
204
+ const inputBindings = React . useMemo ( ( ) => {
205
+ const updateLocalStorage = ( ) => {
206
+ localStorage . set (
207
+ 'inputBindings' ,
208
+ Object . fromEntries (
209
+ Object . entries ( inputBindingsRaw . metadata ) . map ( kv => {
210
+ const [ k , v ] = kv
211
+ return [ k , v . bindings ]
212
+ } )
213
+ )
214
+ )
172
215
}
173
- } , [ shortcutManager ] )
216
+ return {
217
+ /** Transparently pass through `handler()`. */
218
+ get handler ( ) {
219
+ return inputBindingsRaw . handler
220
+ } ,
221
+ /** Transparently pass through `attach()`. */
222
+ get attach ( ) {
223
+ return inputBindingsRaw . attach
224
+ } ,
225
+ reset : ( bindingKey : inputBindingsModule . DashboardBindingKey ) => {
226
+ inputBindingsRaw . reset ( bindingKey )
227
+ updateLocalStorage ( )
228
+ } ,
229
+ add : ( bindingKey : inputBindingsModule . DashboardBindingKey , binding : string ) => {
230
+ inputBindingsRaw . add ( bindingKey , binding )
231
+ updateLocalStorage ( )
232
+ } ,
233
+ delete : ( bindingKey : inputBindingsModule . DashboardBindingKey , binding : string ) => {
234
+ inputBindingsRaw . delete ( bindingKey , binding )
235
+ updateLocalStorage ( )
236
+ } ,
237
+ /** Transparently pass through `metadata`. */
238
+ get metadata ( ) {
239
+ return inputBindingsRaw . metadata
240
+ } ,
241
+ }
242
+ } , [ /* should never change */ localStorage , /* should never change */ inputBindingsRaw ] )
174
243
const mainPageUrl = getMainPageUrl ( )
175
244
const authService = React . useMemo ( ( ) => {
176
245
const authConfig = { navigate, ...props }
@@ -253,9 +322,7 @@ function AppRouter(props: AppProps) {
253
322
</ router . Routes >
254
323
)
255
324
let result = routes
256
- result = (
257
- < ShortcutManagerProvider shortcutManager = { shortcutManager } > { result } </ ShortcutManagerProvider >
258
- )
325
+ result = < InputBindingsProvider inputBindings = { inputBindings } > { result } </ InputBindingsProvider >
259
326
result = < ModalProvider > { result } </ ModalProvider >
260
327
result = (
261
328
< AuthProvider
@@ -269,8 +336,6 @@ function AppRouter(props: AppProps) {
269
336
</ AuthProvider >
270
337
)
271
338
result = < BackendProvider initialBackend = { initialBackend } > { result } </ BackendProvider >
272
- /** {@link BackendProvider } depends on {@link LocalStorageProvider }. */
273
- result = < LocalStorageProvider > { result } </ LocalStorageProvider >
274
339
result = (
275
340
< SessionProvider
276
341
mainPageUrl = { mainPageUrl }
0 commit comments