@tooee/choose 0.1.22 → 0.2.1
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/README.md +28 -2
- package/dist/Choose.d.ts +14 -17
- package/dist/Choose.d.ts.map +1 -1
- package/dist/Choose.js +42 -266
- package/dist/Choose.js.map +1 -1
- package/dist/ChooseFilter.d.ts +12 -0
- package/dist/ChooseFilter.d.ts.map +1 -0
- package/dist/ChooseFilter.js +9 -0
- package/dist/ChooseFilter.js.map +1 -0
- package/dist/ChooseHighlightedText.d.ts +9 -0
- package/dist/ChooseHighlightedText.d.ts.map +1 -0
- package/dist/ChooseHighlightedText.js +30 -0
- package/dist/ChooseHighlightedText.js.map +1 -0
- package/dist/ChooseList.d.ts +28 -0
- package/dist/ChooseList.d.ts.map +1 -0
- package/dist/ChooseList.js +51 -0
- package/dist/ChooseList.js.map +1 -0
- package/dist/ChooseOverlay.d.ts +34 -5
- package/dist/ChooseOverlay.d.ts.map +1 -1
- package/dist/ChooseOverlay.js +29 -200
- package/dist/ChooseOverlay.js.map +1 -1
- package/dist/ChoosePanel.d.ts +30 -0
- package/dist/ChoosePanel.d.ts.map +1 -0
- package/dist/ChoosePanel.js +25 -0
- package/dist/ChoosePanel.js.map +1 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/source.d.ts +5 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +12 -0
- package/dist/source.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-choose.d.ts +80 -0
- package/dist/use-choose.d.ts.map +1 -0
- package/dist/use-choose.js +365 -0
- package/dist/use-choose.js.map +1 -0
- package/package.json +11 -11
- package/src/Choose.tsx +68 -355
- package/src/ChooseFilter.tsx +45 -0
- package/src/ChooseHighlightedText.tsx +42 -0
- package/src/ChooseList.tsx +137 -0
- package/src/ChooseOverlay.tsx +73 -307
- package/src/ChoosePanel.tsx +123 -0
- package/src/index.ts +26 -1
- package/src/source.ts +12 -0
- package/src/types.ts +6 -0
- package/src/use-choose.ts +479 -0
package/src/source.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ChooseItem, ChooseSource } from "./types.js"
|
|
2
|
+
|
|
3
|
+
/** Resolve every public Choose source shape through one compatibility boundary. */
|
|
4
|
+
export function loadChooseSource(source: ChooseSource): ChooseItem[] | Promise<ChooseItem[]> {
|
|
5
|
+
if (Array.isArray(source)) return source
|
|
6
|
+
if (typeof source === "function") return source()
|
|
7
|
+
return source.load()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function chooseSourceError(error: unknown): string {
|
|
11
|
+
return error instanceof Error ? error.message : String(error)
|
|
12
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface ChooseContentProvider {
|
|
|
9
9
|
load(): Promise<ChooseItem[]> | ChooseItem[]
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/** Any item source accepted by the shared chooser core. */
|
|
13
|
+
export type ChooseSource =
|
|
14
|
+
| ChooseItem[]
|
|
15
|
+
| ChooseContentProvider
|
|
16
|
+
| (() => ChooseItem[] | Promise<ChooseItem[]>)
|
|
17
|
+
|
|
12
18
|
export interface ChooseResult {
|
|
13
19
|
items: ChooseItem[]
|
|
14
20
|
}
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
2
|
+
import type { InputRenderable, KeyEvent } from "@opentui/core"
|
|
3
|
+
import {
|
|
4
|
+
useActiveCommandSurface,
|
|
5
|
+
useActions,
|
|
6
|
+
useCommand,
|
|
7
|
+
useCommandSurfaceId,
|
|
8
|
+
useMode,
|
|
9
|
+
useProvideCommandContext,
|
|
10
|
+
useSetMode,
|
|
11
|
+
type ActionDefinition,
|
|
12
|
+
type Mode,
|
|
13
|
+
} from "@tooee/commands"
|
|
14
|
+
import { fuzzyFilter, type FuzzyMatch } from "./fuzzy.js"
|
|
15
|
+
import { chooseSourceError, loadChooseSource } from "./source.js"
|
|
16
|
+
import type { ChooseItem, ChooseResult, ChooseSource } from "./types.js"
|
|
17
|
+
|
|
18
|
+
declare module "@tooee/commands" {
|
|
19
|
+
interface CommandContext {
|
|
20
|
+
/** Contributed by chooser primitives: current filtering and selection state. */
|
|
21
|
+
choose: {
|
|
22
|
+
activeItem: ChooseItem | undefined
|
|
23
|
+
selectedItems: ChooseItem[]
|
|
24
|
+
filterQuery: string
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ChooseCommandGroup = "navigation" | "mode" | "submit" | "cancel" | "multi-select"
|
|
30
|
+
|
|
31
|
+
export interface UseChooseOptions {
|
|
32
|
+
source: ChooseSource
|
|
33
|
+
multi?: boolean
|
|
34
|
+
initialFilter?: string
|
|
35
|
+
onSubmit: (result: ChooseResult) => void | Promise<void>
|
|
36
|
+
onCancel?: () => void
|
|
37
|
+
commands?: ActionDefinition[]
|
|
38
|
+
/** Prefix for built-in command ids (default `choose`). */
|
|
39
|
+
commandScope?: string
|
|
40
|
+
/** Disable a built-in command group when a host owns those keys. */
|
|
41
|
+
disable?: ChooseCommandGroup[]
|
|
42
|
+
/** Explicitly blur/disable mouse views while a legacy covering surface is open. */
|
|
43
|
+
suspended?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ChooseController {
|
|
47
|
+
getFilter(): string
|
|
48
|
+
setFilter(query: string): void
|
|
49
|
+
clearFilter(): void
|
|
50
|
+
moveUp(): void
|
|
51
|
+
moveDown(): void
|
|
52
|
+
setActiveIndex(index: number): void
|
|
53
|
+
getActiveItem(): ChooseItem | undefined
|
|
54
|
+
getSelectedItems(): ChooseItem[]
|
|
55
|
+
toggleActive(): void
|
|
56
|
+
submit(): void
|
|
57
|
+
cancel(): void
|
|
58
|
+
reload(): void
|
|
59
|
+
readonly mode: Mode
|
|
60
|
+
setMode(mode: Mode): void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ChooseState {
|
|
64
|
+
items: ChooseItem[]
|
|
65
|
+
matches: FuzzyMatch[]
|
|
66
|
+
filterQuery: string
|
|
67
|
+
activeIndex: number
|
|
68
|
+
activeItem: ChooseItem | undefined
|
|
69
|
+
selectedOriginalIndices: ReadonlySet<number>
|
|
70
|
+
selectedItems: ChooseItem[]
|
|
71
|
+
loading: boolean
|
|
72
|
+
error: string | null
|
|
73
|
+
multi: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ChooseViewModel {
|
|
77
|
+
filterRef: { current: InputRenderable | null }
|
|
78
|
+
mode: Mode
|
|
79
|
+
/** True while another modal surface (or an explicit host guard) owns interaction. */
|
|
80
|
+
suspended: boolean
|
|
81
|
+
filterFocused: boolean
|
|
82
|
+
onFilterInput: (query: string) => void
|
|
83
|
+
/** Input-level Tab bridge; OpenTUI consumes Tab before global dispatch. */
|
|
84
|
+
onFilterKeyDown: (event: KeyEvent) => void
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface UseChooseResult {
|
|
88
|
+
controller: ChooseController
|
|
89
|
+
state: ChooseState
|
|
90
|
+
view: ChooseViewModel
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Shared headless chooser: source lifecycle, fuzzy matches, selection,
|
|
95
|
+
* command context, controller, and all built-in keyboard commands.
|
|
96
|
+
*/
|
|
97
|
+
export function useChoose(options: UseChooseOptions): UseChooseResult {
|
|
98
|
+
const { source, multi = false, initialFilter = "", commandScope = "choose" } = options
|
|
99
|
+
const initialItems = Array.isArray(source) ? source : []
|
|
100
|
+
|
|
101
|
+
const [items, setItems] = useState<ChooseItem[]>(initialItems)
|
|
102
|
+
const [filterQuery, setFilterQuery] = useState(initialFilter)
|
|
103
|
+
const [activeIndex, setActiveIndexState] = useState(0)
|
|
104
|
+
const [selectedOriginalIndices, setSelectedOriginalIndices] = useState<Set<number>>(new Set())
|
|
105
|
+
const [loading, setLoading] = useState(!Array.isArray(source))
|
|
106
|
+
const [error, setError] = useState<string | null>(null)
|
|
107
|
+
const [reloadRevision, setReloadRevision] = useState(0)
|
|
108
|
+
const filterRef = useRef<InputRenderable>(null)
|
|
109
|
+
const requestIdRef = useRef(0)
|
|
110
|
+
const initialArraySourceRef = useRef<ChooseItem[] | null>(Array.isArray(source) ? source : null)
|
|
111
|
+
const didHandleInitialSourceRef = useRef(false)
|
|
112
|
+
|
|
113
|
+
const matches = useMemo(() => fuzzyFilter(items, filterQuery), [items, filterQuery])
|
|
114
|
+
const activeItem = matches[activeIndex]?.item
|
|
115
|
+
const selectedItems = useMemo(() => {
|
|
116
|
+
if (!multi) return activeItem ? [activeItem] : []
|
|
117
|
+
const selected = Array.from(selectedOriginalIndices).flatMap((index) => {
|
|
118
|
+
const item = items[index]
|
|
119
|
+
return item ? [item] : []
|
|
120
|
+
})
|
|
121
|
+
return selected.length > 0 ? selected : activeItem ? [activeItem] : []
|
|
122
|
+
}, [multi, activeItem, selectedOriginalIndices, items])
|
|
123
|
+
|
|
124
|
+
const mode = useMode()
|
|
125
|
+
const setMode = useSetMode()
|
|
126
|
+
|
|
127
|
+
// Stable controller methods read live values through refs, including when
|
|
128
|
+
// multiple imperative operations happen before React renders again.
|
|
129
|
+
const optionsRef = useRef(options)
|
|
130
|
+
optionsRef.current = options
|
|
131
|
+
const itemsRef = useRef(items)
|
|
132
|
+
itemsRef.current = items
|
|
133
|
+
const matchesRef = useRef(matches)
|
|
134
|
+
matchesRef.current = matches
|
|
135
|
+
const filterQueryRef = useRef(filterQuery)
|
|
136
|
+
filterQueryRef.current = filterQuery
|
|
137
|
+
const activeIndexRef = useRef(activeIndex)
|
|
138
|
+
activeIndexRef.current = activeIndex
|
|
139
|
+
const selectedRef = useRef(selectedOriginalIndices)
|
|
140
|
+
selectedRef.current = selectedOriginalIndices
|
|
141
|
+
const multiRef = useRef(multi)
|
|
142
|
+
multiRef.current = multi
|
|
143
|
+
const modeRef = useRef(mode)
|
|
144
|
+
modeRef.current = mode
|
|
145
|
+
|
|
146
|
+
const replaceItems = useCallback((nextItems: ChooseItem[]) => {
|
|
147
|
+
itemsRef.current = nextItems
|
|
148
|
+
matchesRef.current = fuzzyFilter(nextItems, filterQueryRef.current)
|
|
149
|
+
activeIndexRef.current = 0
|
|
150
|
+
selectedRef.current = new Set()
|
|
151
|
+
setItems(nextItems)
|
|
152
|
+
setActiveIndexState(0)
|
|
153
|
+
setSelectedOriginalIndices(selectedRef.current)
|
|
154
|
+
}, [])
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const requestId = ++requestIdRef.current
|
|
158
|
+
let active = true
|
|
159
|
+
let result: ChooseItem[] | Promise<ChooseItem[]>
|
|
160
|
+
|
|
161
|
+
// Direct arrays seed state during the first render. Avoid a redundant
|
|
162
|
+
// post-mount reset that could overwrite immediate controller operations.
|
|
163
|
+
if (
|
|
164
|
+
!didHandleInitialSourceRef.current &&
|
|
165
|
+
Array.isArray(source) &&
|
|
166
|
+
source === initialArraySourceRef.current
|
|
167
|
+
) {
|
|
168
|
+
didHandleInitialSourceRef.current = true
|
|
169
|
+
setLoading(false)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
didHandleInitialSourceRef.current = true
|
|
173
|
+
|
|
174
|
+
setError(null)
|
|
175
|
+
try {
|
|
176
|
+
result = loadChooseSource(source)
|
|
177
|
+
} catch (loadError) {
|
|
178
|
+
if (requestId !== requestIdRef.current) return
|
|
179
|
+
replaceItems([])
|
|
180
|
+
setError(chooseSourceError(loadError))
|
|
181
|
+
setLoading(false)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (result instanceof Promise) {
|
|
186
|
+
setLoading(true)
|
|
187
|
+
void result.then(
|
|
188
|
+
(loaded) => {
|
|
189
|
+
if (!active || requestId !== requestIdRef.current) return
|
|
190
|
+
replaceItems(loaded)
|
|
191
|
+
setError(null)
|
|
192
|
+
setLoading(false)
|
|
193
|
+
},
|
|
194
|
+
(loadError: unknown) => {
|
|
195
|
+
if (!active || requestId !== requestIdRef.current) return
|
|
196
|
+
replaceItems([])
|
|
197
|
+
setError(chooseSourceError(loadError))
|
|
198
|
+
setLoading(false)
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
return () => {
|
|
202
|
+
active = false
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
replaceItems(result)
|
|
207
|
+
setLoading(false)
|
|
208
|
+
}, [source, reloadRevision, replaceItems])
|
|
209
|
+
|
|
210
|
+
const setFilter = useCallback((query: string) => {
|
|
211
|
+
// Controlled OpenTUI inputs may echo a programmatic `value` update through
|
|
212
|
+
// onInput. Only a real query change resets navigation.
|
|
213
|
+
if (query === filterQueryRef.current) {
|
|
214
|
+
setFilterQuery(query)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
filterQueryRef.current = query
|
|
218
|
+
matchesRef.current = fuzzyFilter(itemsRef.current, query)
|
|
219
|
+
activeIndexRef.current = 0
|
|
220
|
+
setFilterQuery(query)
|
|
221
|
+
setActiveIndexState(0)
|
|
222
|
+
}, [])
|
|
223
|
+
|
|
224
|
+
const setActiveIndex = useCallback((index: number) => {
|
|
225
|
+
const lastIndex = Math.max(0, matchesRef.current.length - 1)
|
|
226
|
+
const next = Math.min(lastIndex, Math.max(0, index))
|
|
227
|
+
activeIndexRef.current = next
|
|
228
|
+
setActiveIndexState(next)
|
|
229
|
+
}, [])
|
|
230
|
+
|
|
231
|
+
const moveUp = useCallback(() => setActiveIndex(activeIndexRef.current - 1), [setActiveIndex])
|
|
232
|
+
const moveDown = useCallback(() => setActiveIndex(activeIndexRef.current + 1), [setActiveIndex])
|
|
233
|
+
|
|
234
|
+
const getActiveItem = useCallback(() => matchesRef.current[activeIndexRef.current]?.item, [])
|
|
235
|
+
|
|
236
|
+
const getSelectedItems = useCallback((): ChooseItem[] => {
|
|
237
|
+
const active = getActiveItem()
|
|
238
|
+
if (!multiRef.current) return active ? [active] : []
|
|
239
|
+
const selected = Array.from(selectedRef.current).flatMap((index) => {
|
|
240
|
+
const item = itemsRef.current[index]
|
|
241
|
+
return item ? [item] : []
|
|
242
|
+
})
|
|
243
|
+
return selected.length > 0 ? selected : active ? [active] : []
|
|
244
|
+
}, [getActiveItem])
|
|
245
|
+
|
|
246
|
+
const toggleActive = useCallback(() => {
|
|
247
|
+
if (!multiRef.current) return
|
|
248
|
+
const originalIndex = matchesRef.current[activeIndexRef.current]?.originalIndex
|
|
249
|
+
if (originalIndex === undefined) return
|
|
250
|
+
const next = new Set(selectedRef.current)
|
|
251
|
+
if (next.has(originalIndex)) next.delete(originalIndex)
|
|
252
|
+
else next.add(originalIndex)
|
|
253
|
+
selectedRef.current = next
|
|
254
|
+
setSelectedOriginalIndices(next)
|
|
255
|
+
}, [])
|
|
256
|
+
|
|
257
|
+
const submit = useCallback(() => {
|
|
258
|
+
void optionsRef.current.onSubmit({ items: getSelectedItems() })
|
|
259
|
+
}, [getSelectedItems])
|
|
260
|
+
|
|
261
|
+
const cancel = useCallback(() => optionsRef.current.onCancel?.(), [])
|
|
262
|
+
const reload = useCallback(() => setReloadRevision((revision) => revision + 1), [])
|
|
263
|
+
const setModeExternal = useCallback((nextMode: Mode) => setMode(nextMode), [setMode])
|
|
264
|
+
|
|
265
|
+
useProvideCommandContext(() => ({
|
|
266
|
+
choose: {
|
|
267
|
+
activeItem: getActiveItem(),
|
|
268
|
+
selectedItems: getSelectedItems(),
|
|
269
|
+
filterQuery: filterQueryRef.current,
|
|
270
|
+
},
|
|
271
|
+
...(optionsRef.current.onCancel ? { exit: cancel } : {}),
|
|
272
|
+
}))
|
|
273
|
+
|
|
274
|
+
useActions(options.commands)
|
|
275
|
+
|
|
276
|
+
const enabled = useCallback(
|
|
277
|
+
(group: ChooseCommandGroup) => !(optionsRef.current.disable?.includes(group) ?? false),
|
|
278
|
+
[],
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
useCommand({
|
|
282
|
+
id: `${commandScope}:escape`,
|
|
283
|
+
title: "Back / cancel",
|
|
284
|
+
hotkey: "Escape",
|
|
285
|
+
modes: ["insert", "cursor"],
|
|
286
|
+
hidden: true,
|
|
287
|
+
when: () =>
|
|
288
|
+
modeRef.current === "insert"
|
|
289
|
+
? enabled("mode")
|
|
290
|
+
: enabled("cancel") && optionsRef.current.onCancel !== undefined,
|
|
291
|
+
handler: () => {
|
|
292
|
+
if (modeRef.current === "insert") setMode("cursor")
|
|
293
|
+
else cancel()
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
useCommand({
|
|
297
|
+
id: `${commandScope}:cancel`,
|
|
298
|
+
title: "Cancel",
|
|
299
|
+
hotkey: "q",
|
|
300
|
+
modes: ["cursor"],
|
|
301
|
+
hidden: true,
|
|
302
|
+
when: () => enabled("cancel") && optionsRef.current.onCancel !== undefined,
|
|
303
|
+
handler: cancel,
|
|
304
|
+
})
|
|
305
|
+
useCommand({
|
|
306
|
+
id: `${commandScope}:insert-mode-i`,
|
|
307
|
+
title: "Insert mode",
|
|
308
|
+
hotkey: "i",
|
|
309
|
+
modes: ["cursor"],
|
|
310
|
+
hidden: true,
|
|
311
|
+
when: () => enabled("mode"),
|
|
312
|
+
handler: () => setMode("insert"),
|
|
313
|
+
})
|
|
314
|
+
useCommand({
|
|
315
|
+
id: `${commandScope}:insert-mode-a`,
|
|
316
|
+
title: "Insert mode",
|
|
317
|
+
hotkey: "a",
|
|
318
|
+
modes: ["cursor"],
|
|
319
|
+
hidden: true,
|
|
320
|
+
when: () => enabled("mode"),
|
|
321
|
+
handler: () => setMode("insert"),
|
|
322
|
+
})
|
|
323
|
+
useCommand({
|
|
324
|
+
id: `${commandScope}:move-down-vim`,
|
|
325
|
+
title: "Move down",
|
|
326
|
+
hotkey: "j",
|
|
327
|
+
modes: ["cursor"],
|
|
328
|
+
hidden: true,
|
|
329
|
+
when: () => enabled("navigation"),
|
|
330
|
+
handler: moveDown,
|
|
331
|
+
})
|
|
332
|
+
useCommand({
|
|
333
|
+
id: `${commandScope}:move-up-vim`,
|
|
334
|
+
title: "Move up",
|
|
335
|
+
hotkey: "k",
|
|
336
|
+
modes: ["cursor"],
|
|
337
|
+
hidden: true,
|
|
338
|
+
when: () => enabled("navigation"),
|
|
339
|
+
handler: moveUp,
|
|
340
|
+
})
|
|
341
|
+
useCommand({
|
|
342
|
+
id: `${commandScope}:confirm`,
|
|
343
|
+
title: "Confirm",
|
|
344
|
+
hotkey: "Enter",
|
|
345
|
+
modes: ["insert", "cursor"],
|
|
346
|
+
hidden: true,
|
|
347
|
+
when: () => enabled("submit"),
|
|
348
|
+
handler: submit,
|
|
349
|
+
})
|
|
350
|
+
useCommand({
|
|
351
|
+
id: `${commandScope}:move-up`,
|
|
352
|
+
title: "Move up",
|
|
353
|
+
hotkey: "up",
|
|
354
|
+
modes: ["insert", "cursor"],
|
|
355
|
+
hidden: true,
|
|
356
|
+
when: () => enabled("navigation"),
|
|
357
|
+
handler: moveUp,
|
|
358
|
+
})
|
|
359
|
+
useCommand({
|
|
360
|
+
id: `${commandScope}:move-up-ctrl-p`,
|
|
361
|
+
title: "Move up",
|
|
362
|
+
hotkey: "ctrl+p",
|
|
363
|
+
modes: ["insert", "cursor"],
|
|
364
|
+
hidden: true,
|
|
365
|
+
when: () => enabled("navigation"),
|
|
366
|
+
handler: moveUp,
|
|
367
|
+
})
|
|
368
|
+
useCommand({
|
|
369
|
+
id: `${commandScope}:move-down`,
|
|
370
|
+
title: "Move down",
|
|
371
|
+
hotkey: "down",
|
|
372
|
+
modes: ["insert", "cursor"],
|
|
373
|
+
hidden: true,
|
|
374
|
+
when: () => enabled("navigation"),
|
|
375
|
+
handler: moveDown,
|
|
376
|
+
})
|
|
377
|
+
useCommand({
|
|
378
|
+
id: `${commandScope}:move-down-ctrl-n`,
|
|
379
|
+
title: "Move down",
|
|
380
|
+
hotkey: "ctrl+n",
|
|
381
|
+
modes: ["insert", "cursor"],
|
|
382
|
+
hidden: true,
|
|
383
|
+
when: () => enabled("navigation"),
|
|
384
|
+
handler: moveDown,
|
|
385
|
+
})
|
|
386
|
+
useCommand({
|
|
387
|
+
id: `${commandScope}:toggle-next`,
|
|
388
|
+
title: "Toggle selection and move down",
|
|
389
|
+
hotkey: "Tab",
|
|
390
|
+
modes: ["insert", "cursor"],
|
|
391
|
+
hidden: true,
|
|
392
|
+
when: () => enabled("multi-select") && multiRef.current,
|
|
393
|
+
handler: () => {
|
|
394
|
+
toggleActive()
|
|
395
|
+
moveDown()
|
|
396
|
+
},
|
|
397
|
+
})
|
|
398
|
+
useCommand({
|
|
399
|
+
id: `${commandScope}:toggle-previous`,
|
|
400
|
+
title: "Toggle selection and move up",
|
|
401
|
+
hotkey: "shift+Tab",
|
|
402
|
+
modes: ["insert", "cursor"],
|
|
403
|
+
hidden: true,
|
|
404
|
+
when: () => enabled("multi-select") && multiRef.current,
|
|
405
|
+
handler: () => {
|
|
406
|
+
toggleActive()
|
|
407
|
+
moveUp()
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
const surfaceId = useCommandSurfaceId()
|
|
412
|
+
const activeSurface = useActiveCommandSurface()
|
|
413
|
+
const suspended =
|
|
414
|
+
(options.suspended ?? false) || (activeSurface !== null && activeSurface.id !== surfaceId)
|
|
415
|
+
|
|
416
|
+
const onFilterKeyDown = useCallback(
|
|
417
|
+
(event: KeyEvent) => {
|
|
418
|
+
if (
|
|
419
|
+
event.name !== "tab" ||
|
|
420
|
+
!multiRef.current ||
|
|
421
|
+
!enabled("multi-select") ||
|
|
422
|
+
modeRef.current !== "insert"
|
|
423
|
+
) {
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
event.preventDefault()
|
|
427
|
+
toggleActive()
|
|
428
|
+
if (event.shift) moveUp()
|
|
429
|
+
else moveDown()
|
|
430
|
+
},
|
|
431
|
+
[enabled, moveDown, moveUp, toggleActive],
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
const controllerRef = useRef<ChooseController | null>(null)
|
|
435
|
+
if (controllerRef.current === null) {
|
|
436
|
+
controllerRef.current = {
|
|
437
|
+
getFilter: () => filterQueryRef.current,
|
|
438
|
+
setFilter,
|
|
439
|
+
clearFilter: () => setFilter(""),
|
|
440
|
+
moveUp,
|
|
441
|
+
moveDown,
|
|
442
|
+
setActiveIndex,
|
|
443
|
+
getActiveItem,
|
|
444
|
+
getSelectedItems,
|
|
445
|
+
toggleActive,
|
|
446
|
+
submit,
|
|
447
|
+
cancel,
|
|
448
|
+
reload,
|
|
449
|
+
get mode() {
|
|
450
|
+
return modeRef.current
|
|
451
|
+
},
|
|
452
|
+
setMode: setModeExternal,
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
controller: controllerRef.current,
|
|
458
|
+
state: {
|
|
459
|
+
items,
|
|
460
|
+
matches,
|
|
461
|
+
filterQuery,
|
|
462
|
+
activeIndex,
|
|
463
|
+
activeItem,
|
|
464
|
+
selectedOriginalIndices,
|
|
465
|
+
selectedItems,
|
|
466
|
+
loading,
|
|
467
|
+
error,
|
|
468
|
+
multi,
|
|
469
|
+
},
|
|
470
|
+
view: {
|
|
471
|
+
filterRef,
|
|
472
|
+
mode,
|
|
473
|
+
suspended,
|
|
474
|
+
filterFocused: mode === "insert" && !suspended,
|
|
475
|
+
onFilterInput: setFilter,
|
|
476
|
+
onFilterKeyDown,
|
|
477
|
+
},
|
|
478
|
+
}
|
|
479
|
+
}
|