@tanstack/devtools 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk/{G64KXXVZ.js → YGJWPK3G.js} +57 -55
- package/dist/dev.js +3 -3
- package/dist/devtools/{F57NNUQX.js → CMWGZDSU.js} +159 -105
- package/dist/devtools/{OLELRPKB.js → LWN3DL7E.js} +191 -129
- package/dist/index.d.ts +9 -2
- package/dist/index.js +3 -3
- package/dist/server.js +2 -2
- package/package.json +2 -2
- package/src/components/source-inspector.tsx +6 -5
- package/src/context/devtools-context.tsx +2 -0
- package/src/context/devtools-store.ts +10 -2
- package/src/context/use-devtools-context.ts +8 -0
- package/src/devtools.tsx +5 -14
- package/src/styles/use-styles.ts +5 -0
- package/src/tabs/hotkey-config.tsx +97 -0
- package/src/tabs/settings-tab.tsx +23 -86
- package/src/utils/hotkey.test.ts +109 -0
- package/src/utils/hotkey.ts +66 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { TabName } from '../tabs'
|
|
2
2
|
import type { TanStackDevtoolsPlugin } from './devtools-context'
|
|
3
3
|
|
|
4
|
-
type ModifierKey = 'Alt' | 'Control' | 'Meta' | 'Shift'
|
|
4
|
+
type ModifierKey = 'Alt' | 'Control' | 'Meta' | 'Shift' | 'CtrlOrMeta'
|
|
5
5
|
type KeyboardKey = ModifierKey | (string & {})
|
|
6
|
+
export type { ModifierKey, KeyboardKey }
|
|
6
7
|
export const keyboardModifiers: Array<ModifierKey> = [
|
|
7
8
|
'Alt',
|
|
8
9
|
'Control',
|
|
9
10
|
'Meta',
|
|
10
11
|
'Shift',
|
|
12
|
+
'CtrlOrMeta',
|
|
11
13
|
]
|
|
12
14
|
|
|
13
15
|
type TriggerPosition =
|
|
@@ -47,9 +49,14 @@ export type DevtoolsStore = {
|
|
|
47
49
|
panelLocation: 'top' | 'bottom'
|
|
48
50
|
/**
|
|
49
51
|
* The hotkey to open the dev tools
|
|
50
|
-
* @default "
|
|
52
|
+
* @default ["Shift", "A"]
|
|
51
53
|
*/
|
|
52
54
|
openHotkey: Array<KeyboardKey>
|
|
55
|
+
/**
|
|
56
|
+
* The hotkey to open the source inspector
|
|
57
|
+
* @default ["Shift", "CtrlOrMeta"]
|
|
58
|
+
*/
|
|
59
|
+
inspectHotkey: Array<KeyboardKey>
|
|
53
60
|
/**
|
|
54
61
|
* Whether to require the URL flag to open the dev tools
|
|
55
62
|
* @default false
|
|
@@ -93,6 +100,7 @@ export const initialState: DevtoolsStore = {
|
|
|
93
100
|
position: 'bottom-right',
|
|
94
101
|
panelLocation: 'bottom',
|
|
95
102
|
openHotkey: ['Shift', 'A'],
|
|
103
|
+
inspectHotkey: ['Shift', 'CtrlOrMeta'],
|
|
96
104
|
requireUrlFlag: false,
|
|
97
105
|
urlFlag: 'tanstack-devtools',
|
|
98
106
|
theme:
|
|
@@ -48,6 +48,14 @@ export const usePlugins = () => {
|
|
|
48
48
|
setStore((prev) => {
|
|
49
49
|
const isActive = prev.state.activePlugins.includes(pluginId)
|
|
50
50
|
|
|
51
|
+
const currentPlugin = store.plugins?.find(
|
|
52
|
+
(plugin) => plugin.id === pluginId,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if (currentPlugin?.destroy && isActive) {
|
|
56
|
+
currentPlugin.destroy(pluginId)
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
const updatedPlugins = isActive
|
|
52
60
|
? prev.state.activePlugins.filter((id) => id !== pluginId)
|
|
53
61
|
: [...prev.state.activePlugins, pluginId]
|
package/src/devtools.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { createShortcut } from '@solid-primitives/keyboard'
|
|
|
3
3
|
import { Portal } from 'solid-js/web'
|
|
4
4
|
import { ThemeContextProvider } from '@tanstack/devtools-ui'
|
|
5
5
|
import { devtoolsEventClient } from '@tanstack/devtools-client'
|
|
6
|
+
|
|
6
7
|
import {
|
|
7
8
|
useDevtoolsSettings,
|
|
8
9
|
useHeight,
|
|
@@ -11,13 +12,12 @@ import {
|
|
|
11
12
|
} from './context/use-devtools-context'
|
|
12
13
|
import { useDisableTabbing } from './hooks/use-disable-tabbing'
|
|
13
14
|
import { TANSTACK_DEVTOOLS } from './utils/storage'
|
|
15
|
+
import { getHotkeyPermutations } from './utils/hotkey'
|
|
14
16
|
import { Trigger } from './components/trigger'
|
|
15
17
|
import { MainPanel } from './components/main-panel'
|
|
16
18
|
import { ContentPanel } from './components/content-panel'
|
|
17
19
|
import { Tabs } from './components/tabs'
|
|
18
20
|
import { TabContent } from './components/tab-content'
|
|
19
|
-
import { keyboardModifiers } from './context/devtools-store'
|
|
20
|
-
import { getAllPermutations } from './utils/sanitize'
|
|
21
21
|
import { usePiPWindow } from './context/pip-context'
|
|
22
22
|
import { SourceInspector } from './components/source-inspector'
|
|
23
23
|
|
|
@@ -165,18 +165,9 @@ export default function DevTools() {
|
|
|
165
165
|
}
|
|
166
166
|
})
|
|
167
167
|
createEffect(() => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
const nonModifiers = settings().openHotkey.filter(
|
|
173
|
-
(key) => !keyboardModifiers.includes(key as any),
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
const allModifierCombinations = getAllPermutations(modifiers)
|
|
177
|
-
|
|
178
|
-
for (const combination of allModifierCombinations) {
|
|
179
|
-
const permutation = [...combination, ...nonModifiers]
|
|
168
|
+
const permutations = getHotkeyPermutations(settings().openHotkey)
|
|
169
|
+
|
|
170
|
+
for (const permutation of permutations) {
|
|
180
171
|
createShortcut(permutation, () => {
|
|
181
172
|
toggleOpen()
|
|
182
173
|
})
|
package/src/styles/use-styles.ts
CHANGED
|
@@ -466,6 +466,11 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => {
|
|
|
466
466
|
display: flex;
|
|
467
467
|
gap: 0.5rem;
|
|
468
468
|
`,
|
|
469
|
+
settingsStack: css`
|
|
470
|
+
display: flex;
|
|
471
|
+
flex-direction: column;
|
|
472
|
+
gap: 1rem;
|
|
473
|
+
`,
|
|
469
474
|
|
|
470
475
|
// No Plugins Fallback Styles
|
|
471
476
|
noPluginsFallback: css`
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Show } from 'solid-js'
|
|
2
|
+
import { Button, Input } from '@tanstack/devtools-ui'
|
|
3
|
+
|
|
4
|
+
import { uppercaseFirstLetter } from '../utils/sanitize'
|
|
5
|
+
import { useStyles } from '../styles/use-styles'
|
|
6
|
+
import type { KeyboardKey } from '../context/devtools-store'
|
|
7
|
+
|
|
8
|
+
interface HotkeyConfigProps {
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
hotkey: Array<KeyboardKey>
|
|
12
|
+
modifiers: Array<KeyboardKey>
|
|
13
|
+
onHotkeyChange: (hotkey: Array<KeyboardKey>) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MODIFIER_DISPLAY_NAMES: Record<KeyboardKey, string> = {
|
|
17
|
+
Shift: 'Shift',
|
|
18
|
+
Alt: 'Alt',
|
|
19
|
+
Meta: 'Meta',
|
|
20
|
+
Control: 'Control',
|
|
21
|
+
CtrlOrMeta: 'Ctrl Or Meta',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const HotkeyConfig = (props: HotkeyConfigProps) => {
|
|
25
|
+
const styles = useStyles()
|
|
26
|
+
|
|
27
|
+
const toggleModifier = (modifier: KeyboardKey) => {
|
|
28
|
+
if (props.hotkey.includes(modifier)) {
|
|
29
|
+
props.onHotkeyChange(props.hotkey.filter((key) => key !== modifier))
|
|
30
|
+
} else {
|
|
31
|
+
const existingModifiers = props.hotkey.filter((key) =>
|
|
32
|
+
props.modifiers.includes(key as any),
|
|
33
|
+
)
|
|
34
|
+
const otherKeys = props.hotkey.filter(
|
|
35
|
+
(key) => !props.modifiers.includes(key as any),
|
|
36
|
+
)
|
|
37
|
+
props.onHotkeyChange([...existingModifiers, modifier, ...otherKeys])
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const getNonModifierValue = () => {
|
|
42
|
+
return props.hotkey
|
|
43
|
+
.filter((key) => !props.modifiers.includes(key as any))
|
|
44
|
+
.join('+')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const handleKeyInput = (input: string) => {
|
|
48
|
+
const makeModifierArray = (key: string) => {
|
|
49
|
+
if (key.length === 1) return [uppercaseFirstLetter(key)]
|
|
50
|
+
const modifiersArray: Array<string> = []
|
|
51
|
+
for (const character of key) {
|
|
52
|
+
const newLetter = uppercaseFirstLetter(character)
|
|
53
|
+
if (!modifiersArray.includes(newLetter)) modifiersArray.push(newLetter)
|
|
54
|
+
}
|
|
55
|
+
return modifiersArray
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hotkeyModifiers = props.hotkey.filter((key) =>
|
|
59
|
+
props.modifiers.includes(key as any),
|
|
60
|
+
)
|
|
61
|
+
const newKeys = input
|
|
62
|
+
.split('+')
|
|
63
|
+
.flatMap((key) => makeModifierArray(key))
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
props.onHotkeyChange([...hotkeyModifiers, ...newKeys])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const getDisplayHotkey = () => {
|
|
69
|
+
return props.hotkey.join(' + ')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div class={styles().settingsGroup}>
|
|
74
|
+
<h4 style={{ margin: 0 }}>{props.description}</h4>
|
|
75
|
+
<div class={styles().settingsModifiers}>
|
|
76
|
+
<Show keyed when={props.hotkey}>
|
|
77
|
+
{props.modifiers.map((modifier) => (
|
|
78
|
+
<Button
|
|
79
|
+
variant="success"
|
|
80
|
+
onclick={() => toggleModifier(modifier)}
|
|
81
|
+
outline={!props.hotkey.includes(modifier)}
|
|
82
|
+
>
|
|
83
|
+
{MODIFIER_DISPLAY_NAMES[modifier] || modifier}
|
|
84
|
+
</Button>
|
|
85
|
+
))}
|
|
86
|
+
</Show>
|
|
87
|
+
</div>
|
|
88
|
+
<Input
|
|
89
|
+
description="Use '+' to combine keys (e.g., 'a+b' or 'd'). This will be used with the enabled modifiers from above"
|
|
90
|
+
placeholder="a"
|
|
91
|
+
value={getNonModifierValue()}
|
|
92
|
+
onChange={handleKeyInput}
|
|
93
|
+
/>
|
|
94
|
+
Final shortcut is: {getDisplayHotkey()}
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Show
|
|
1
|
+
import { Show } from 'solid-js'
|
|
2
2
|
import {
|
|
3
|
-
Button,
|
|
4
3
|
Checkbox,
|
|
5
4
|
Input,
|
|
6
5
|
MainPanel,
|
|
@@ -16,32 +15,18 @@ import {
|
|
|
16
15
|
Link,
|
|
17
16
|
SettingsCog,
|
|
18
17
|
} from '@tanstack/devtools-ui/icons'
|
|
18
|
+
|
|
19
19
|
import { useDevtoolsSettings } from '../context/use-devtools-context'
|
|
20
|
-
import { uppercaseFirstLetter } from '../utils/sanitize'
|
|
21
20
|
import { useStyles } from '../styles/use-styles'
|
|
22
|
-
import
|
|
21
|
+
import { HotkeyConfig } from './hotkey-config'
|
|
22
|
+
import type { KeyboardKey } from '../context/devtools-store'
|
|
23
23
|
|
|
24
24
|
export const SettingsTab = () => {
|
|
25
25
|
const { setSettings, settings } = useDevtoolsSettings()
|
|
26
26
|
const styles = useStyles()
|
|
27
|
-
|
|
28
|
-
const modifiers: Array<
|
|
29
|
-
|
|
30
|
-
if (hotkey().includes(newHotkey)) {
|
|
31
|
-
return setSettings({
|
|
32
|
-
openHotkey: hotkey().filter((key) => key !== newHotkey),
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
const existingModifiers = hotkey().filter((key) =>
|
|
36
|
-
modifiers.includes(key as any),
|
|
37
|
-
)
|
|
38
|
-
const otherModifiers = hotkey().filter(
|
|
39
|
-
(key) => !modifiers.includes(key as any),
|
|
40
|
-
)
|
|
41
|
-
setSettings({
|
|
42
|
-
openHotkey: [...existingModifiers, newHotkey, ...otherModifiers],
|
|
43
|
-
})
|
|
44
|
-
}
|
|
27
|
+
|
|
28
|
+
const modifiers: Array<KeyboardKey> = ['CtrlOrMeta', 'Alt', 'Shift']
|
|
29
|
+
|
|
45
30
|
return (
|
|
46
31
|
<MainPanel withPadding>
|
|
47
32
|
<Section>
|
|
@@ -144,71 +129,23 @@ export const SettingsTab = () => {
|
|
|
144
129
|
<SectionDescription>
|
|
145
130
|
Customize keyboard shortcuts for quick access.
|
|
146
131
|
</SectionDescription>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
</Button>
|
|
164
|
-
<Button
|
|
165
|
-
variant="success"
|
|
166
|
-
onclick={changeHotkey('Meta')}
|
|
167
|
-
outline={!hotkey().includes('Meta')}
|
|
168
|
-
>
|
|
169
|
-
Meta
|
|
170
|
-
</Button>
|
|
171
|
-
<Button
|
|
172
|
-
variant="success"
|
|
173
|
-
onclick={changeHotkey('Control')}
|
|
174
|
-
outline={!hotkey().includes('Control')}
|
|
175
|
-
>
|
|
176
|
-
Control
|
|
177
|
-
</Button>
|
|
178
|
-
</Show>
|
|
179
|
-
</div>
|
|
180
|
-
<Input
|
|
181
|
-
label="Hotkey to open/close devtools"
|
|
182
|
-
description="Use '+' to combine keys (e.g., 'a+b' or 'd'). This will be used with the enabled modifiers from above"
|
|
183
|
-
placeholder="a"
|
|
184
|
-
value={hotkey()
|
|
185
|
-
.filter((key) => !['Shift', 'Meta', 'Alt', 'Ctrl'].includes(key))
|
|
186
|
-
.join('+')}
|
|
187
|
-
onChange={(e) => {
|
|
188
|
-
const makeModifierArray = (key: string) => {
|
|
189
|
-
if (key.length === 1) return [uppercaseFirstLetter(key)]
|
|
190
|
-
const modifiers: Array<string> = []
|
|
191
|
-
for (const character of key) {
|
|
192
|
-
const newLetter = uppercaseFirstLetter(character)
|
|
193
|
-
if (!modifiers.includes(newLetter)) modifiers.push(newLetter)
|
|
194
|
-
}
|
|
195
|
-
return modifiers
|
|
196
|
-
}
|
|
197
|
-
const modifiers = e
|
|
198
|
-
.split('+')
|
|
199
|
-
.flatMap((key) => makeModifierArray(key))
|
|
200
|
-
.filter(Boolean)
|
|
201
|
-
return setSettings({
|
|
202
|
-
openHotkey: [
|
|
203
|
-
...hotkey().filter((key) =>
|
|
204
|
-
['Shift', 'Meta', 'Alt', 'Ctrl'].includes(key),
|
|
205
|
-
),
|
|
206
|
-
...modifiers,
|
|
207
|
-
],
|
|
208
|
-
})
|
|
209
|
-
}}
|
|
132
|
+
|
|
133
|
+
<div class={styles().settingsStack}>
|
|
134
|
+
<HotkeyConfig
|
|
135
|
+
title="Open/Close Devtools"
|
|
136
|
+
description="Hotkey to open/close devtools"
|
|
137
|
+
hotkey={settings().openHotkey}
|
|
138
|
+
modifiers={modifiers}
|
|
139
|
+
onHotkeyChange={(hotkey) => setSettings({ openHotkey: hotkey })}
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<HotkeyConfig
|
|
143
|
+
title="Source Inspector"
|
|
144
|
+
description="Hotkey to open source inspector"
|
|
145
|
+
hotkey={settings().inspectHotkey}
|
|
146
|
+
modifiers={modifiers}
|
|
147
|
+
onHotkeyChange={(hotkey) => setSettings({ inspectHotkey: hotkey })}
|
|
210
148
|
/>
|
|
211
|
-
Final shortcut is: {hotkey().join(' + ')}
|
|
212
149
|
</div>
|
|
213
150
|
</Section>
|
|
214
151
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getHotkeyPermutations,
|
|
4
|
+
isHotkeyCombinationPressed,
|
|
5
|
+
normalizeHotkey,
|
|
6
|
+
} from './hotkey'
|
|
7
|
+
import type { KeyboardKey } from '../context/devtools-store'
|
|
8
|
+
|
|
9
|
+
describe('hotkey utilities', () => {
|
|
10
|
+
describe('normalizeHotkey', () => {
|
|
11
|
+
it('should return unchanged array when CtrlOrMeta is not present', () => {
|
|
12
|
+
const hotkey: Array<KeyboardKey> = ['Shift', 'A']
|
|
13
|
+
const result = normalizeHotkey(hotkey)
|
|
14
|
+
expect(result).toEqual([['Shift', 'A']])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should expand CtrlOrMeta to Control and Meta variants', () => {
|
|
18
|
+
const hotkey: Array<KeyboardKey> = ['Shift', 'CtrlOrMeta']
|
|
19
|
+
const result = normalizeHotkey(hotkey)
|
|
20
|
+
expect(result).toHaveLength(2)
|
|
21
|
+
expect(result).toContainEqual(['Shift', 'Control'])
|
|
22
|
+
expect(result).toContainEqual(['Shift', 'Meta'])
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('getHotkeyPermutations', () => {
|
|
27
|
+
it('should generate permutations for modifiers in any order', () => {
|
|
28
|
+
const hotkey: Array<KeyboardKey> = ['Shift', 'Control', 'A']
|
|
29
|
+
const result = getHotkeyPermutations(hotkey)
|
|
30
|
+
expect(result).toContainEqual(['Shift', 'Control', 'A'])
|
|
31
|
+
expect(result).toContainEqual(['Control', 'Shift', 'A'])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should handle CtrlOrMeta expansion with multiple permutations', () => {
|
|
35
|
+
const hotkey: Array<KeyboardKey> = ['Shift', 'CtrlOrMeta']
|
|
36
|
+
const result = getHotkeyPermutations(hotkey)
|
|
37
|
+
expect(result).toContainEqual(['Shift', 'Control'])
|
|
38
|
+
expect(result).toContainEqual(['Control', 'Shift'])
|
|
39
|
+
expect(result).toContainEqual(['Shift', 'Meta'])
|
|
40
|
+
expect(result).toContainEqual(['Meta', 'Shift'])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should handle single key hotkey with no modifiers', () => {
|
|
44
|
+
const hotkey: Array<KeyboardKey> = ['A']
|
|
45
|
+
const result = getHotkeyPermutations(hotkey)
|
|
46
|
+
expect(result).toEqual([['A']])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should not have duplicate permutations', () => {
|
|
50
|
+
const hotkey: Array<KeyboardKey> = ['Shift', 'Alt', 'A']
|
|
51
|
+
const result = getHotkeyPermutations(hotkey)
|
|
52
|
+
const stringified = result.map((combo) => JSON.stringify(combo))
|
|
53
|
+
const unique = new Set(stringified)
|
|
54
|
+
expect(unique.size).toBe(stringified.length)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('isHotkeyCombinationPressed', () => {
|
|
59
|
+
it('should match exact key combination', () => {
|
|
60
|
+
expect(isHotkeyCombinationPressed(['Shift', 'A'], ['Shift', 'A'])).toBe(
|
|
61
|
+
true,
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should be case-insensitive', () => {
|
|
66
|
+
expect(isHotkeyCombinationPressed(['shift', 'a'], ['Shift', 'A'])).toBe(
|
|
67
|
+
true,
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should match regardless of modifier order', () => {
|
|
72
|
+
expect(
|
|
73
|
+
isHotkeyCombinationPressed(
|
|
74
|
+
['A', 'Control', 'Shift'],
|
|
75
|
+
['Shift', 'Control', 'A'],
|
|
76
|
+
),
|
|
77
|
+
).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should handle CtrlOrMeta with Control', () => {
|
|
81
|
+
expect(
|
|
82
|
+
isHotkeyCombinationPressed(
|
|
83
|
+
['Shift', 'Control'],
|
|
84
|
+
['Shift', 'CtrlOrMeta'],
|
|
85
|
+
),
|
|
86
|
+
).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should handle CtrlOrMeta with Meta', () => {
|
|
90
|
+
expect(
|
|
91
|
+
isHotkeyCombinationPressed(['Shift', 'Meta'], ['Shift', 'CtrlOrMeta']),
|
|
92
|
+
).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should reject incomplete key combinations', () => {
|
|
96
|
+
expect(isHotkeyCombinationPressed(['Shift'], ['Shift', 'A'])).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should reject extra keys', () => {
|
|
100
|
+
expect(
|
|
101
|
+
isHotkeyCombinationPressed(['Shift', 'A', 'B'], ['Shift', 'A']),
|
|
102
|
+
).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should handle single key hotkey', () => {
|
|
106
|
+
expect(isHotkeyCombinationPressed(['A'], ['A'])).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { keyboardModifiers } from '../context/devtools-store'
|
|
2
|
+
import { getAllPermutations } from './sanitize'
|
|
3
|
+
|
|
4
|
+
import type { KeyboardKey, ModifierKey } from '../context/devtools-store'
|
|
5
|
+
|
|
6
|
+
/** Expands CtrlOrMeta into separate Control and Meta variants */
|
|
7
|
+
export const normalizeHotkey = (
|
|
8
|
+
keys: Array<KeyboardKey>,
|
|
9
|
+
): Array<Array<KeyboardKey>> => {
|
|
10
|
+
// no normalization needed if CtrlOrMeta not used
|
|
11
|
+
if (!keys.includes('CtrlOrMeta')) {
|
|
12
|
+
return [keys]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return [
|
|
16
|
+
keys.map((key) => (key === 'CtrlOrMeta' ? 'Control' : key)),
|
|
17
|
+
keys.map((key) => (key === 'CtrlOrMeta' ? 'Meta' : key)),
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generates all keyboard permutations for a given hotkey configuration
|
|
23
|
+
* Handles CtrlOrMeta expansion and creates all possible combinations
|
|
24
|
+
*/
|
|
25
|
+
export const getHotkeyPermutations = (
|
|
26
|
+
hotkey: Array<KeyboardKey>,
|
|
27
|
+
): Array<Array<KeyboardKey>> => {
|
|
28
|
+
const normalizedHotkeys = normalizeHotkey(hotkey)
|
|
29
|
+
|
|
30
|
+
return normalizedHotkeys.flatMap((normalizedHotkey) => {
|
|
31
|
+
const modifiers = normalizedHotkey.filter((key) =>
|
|
32
|
+
keyboardModifiers.includes(key as any),
|
|
33
|
+
) as Array<ModifierKey>
|
|
34
|
+
|
|
35
|
+
const nonModifiers = normalizedHotkey.filter(
|
|
36
|
+
(key) => !keyboardModifiers.includes(key as any),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// handle case with no modifiers (just non-modifier keys)
|
|
40
|
+
if (modifiers.length === 0) {
|
|
41
|
+
return [nonModifiers]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const allModifierCombinations = getAllPermutations(modifiers)
|
|
45
|
+
return allModifierCombinations.map((combo) => [...combo, ...nonModifiers])
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Checks if the currently pressed keys match any of the hotkey permutations */
|
|
50
|
+
export const isHotkeyCombinationPressed = (
|
|
51
|
+
keys: Array<string>,
|
|
52
|
+
hotkey: Array<KeyboardKey>,
|
|
53
|
+
): boolean => {
|
|
54
|
+
const permutations = getHotkeyPermutations(hotkey)
|
|
55
|
+
const pressedKeys = keys.map((key) => key.toUpperCase())
|
|
56
|
+
|
|
57
|
+
return permutations.some(
|
|
58
|
+
(combo) =>
|
|
59
|
+
// every key in the combo must be pressed
|
|
60
|
+
combo.every((key) => pressedKeys.includes(String(key).toUpperCase())) &&
|
|
61
|
+
// and no extra keys beyond the combo
|
|
62
|
+
pressedKeys.every((key) =>
|
|
63
|
+
combo.map((k) => String(k).toUpperCase()).includes(key),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
}
|