|
@@ -22,6 +22,7 @@ import { formatKeyBindings, useBindings, useKeymapSelector } from "../keymap"
|
|
|
|
|
|
|
|
export interface DialogSelectProps<T> {
|
|
export interface DialogSelectProps<T> {
|
|
|
title: string
|
|
title: string
|
|
|
|
|
+ titleView?: JSX.Element
|
|
|
placeholder?: string
|
|
placeholder?: string
|
|
|
footer?: JSX.Element
|
|
footer?: JSX.Element
|
|
|
options: DialogSelectOption<T>[]
|
|
options: DialogSelectOption<T>[]
|
|
@@ -32,11 +33,13 @@ export interface DialogSelectProps<T> {
|
|
|
onSelect?: (option: DialogSelectOption<T>) => void
|
|
onSelect?: (option: DialogSelectOption<T>) => void
|
|
|
skipFilter?: boolean
|
|
skipFilter?: boolean
|
|
|
renderFilter?: boolean
|
|
renderFilter?: boolean
|
|
|
|
|
+ locked?: boolean
|
|
|
actions?: {
|
|
actions?: {
|
|
|
command: string
|
|
command: string
|
|
|
title: string
|
|
title: string
|
|
|
side?: "left" | "right"
|
|
side?: "left" | "right"
|
|
|
- disabled?: boolean
|
|
|
|
|
|
|
+ hidden?: boolean
|
|
|
|
|
+ disabled?: boolean | ((option: DialogSelectOption<T> | undefined) => boolean)
|
|
|
onTrigger: (option: DialogSelectOption<T>) => void
|
|
onTrigger: (option: DialogSelectOption<T>) => void
|
|
|
}[]
|
|
}[]
|
|
|
footerHints?: {
|
|
footerHints?: {
|
|
@@ -108,17 +111,18 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
let input: InputRenderable
|
|
let input: InputRenderable
|
|
|
|
|
|
|
|
const actions = createMemo(() => props.actions ?? [])
|
|
const actions = createMemo(() => props.actions ?? [])
|
|
|
|
|
+ const shownActions = createMemo(() => actions().filter((item) => !item.hidden))
|
|
|
const actionBindings = useKeymapSelector((keymap) =>
|
|
const actionBindings = useKeymapSelector((keymap) =>
|
|
|
keymap.getCommandBindings({
|
|
keymap.getCommandBindings({
|
|
|
visibility: "registered",
|
|
visibility: "registered",
|
|
|
- commands: actions().map((item) => item.command),
|
|
|
|
|
|
|
+ commands: shownActions().map((item) => item.command),
|
|
|
}),
|
|
}),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const actionLabels = createMemo(() => {
|
|
const actionLabels = createMemo(() => {
|
|
|
const labels = new Map<string, string>()
|
|
const labels = new Map<string, string>()
|
|
|
|
|
|
|
|
- for (const action of actions()) {
|
|
|
|
|
|
|
+ for (const action of shownActions()) {
|
|
|
const label = formatKeyBindings(actionBindings().get(action.command), tuiConfig)
|
|
const label = formatKeyBindings(actionBindings().get(action.command), tuiConfig)
|
|
|
if (label) labels.set(action.command, label)
|
|
if (label) labels.set(action.command, label)
|
|
|
}
|
|
}
|
|
@@ -126,12 +130,12 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
return labels
|
|
return labels
|
|
|
})
|
|
})
|
|
|
const visibleActions = createMemo(() => [
|
|
const visibleActions = createMemo(() => [
|
|
|
- ...actions()
|
|
|
|
|
|
|
+ ...shownActions()
|
|
|
.map((item) => ({ ...item, label: actionLabels().get(item.command) ?? "" }))
|
|
.map((item) => ({ ...item, label: actionLabels().get(item.command) ?? "" }))
|
|
|
- .filter((item) => !item.disabled && item.label),
|
|
|
|
|
|
|
+ .filter((item) => item.label),
|
|
|
...(props.footerHints ?? []),
|
|
...(props.footerHints ?? []),
|
|
|
])
|
|
])
|
|
|
- const actionItems = createMemo(() => visibleActions().filter(isActionItem))
|
|
|
|
|
|
|
+ const actionItems = createMemo(() => visibleActions().filter(isActionItem).filter((item) => !isActionDisabled(item)))
|
|
|
|
|
|
|
|
createEffect(() => {
|
|
createEffect(() => {
|
|
|
const index = focusedAction()
|
|
const index = focusedAction()
|
|
@@ -217,6 +221,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
function move(direction: number) {
|
|
function move(direction: number) {
|
|
|
|
|
+ if (props.locked) return
|
|
|
if (flat().length === 0) return
|
|
if (flat().length === 0) return
|
|
|
let next = store.selected + direction
|
|
let next = store.selected + direction
|
|
|
if (next < 0) next = flat().length - 1
|
|
if (next < 0) next = flat().length - 1
|
|
@@ -252,6 +257,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function submit() {
|
|
function submit() {
|
|
|
|
|
+ if (props.locked) return
|
|
|
setStore("input", "keyboard")
|
|
setStore("input", "keyboard")
|
|
|
const index = focusedAction()
|
|
const index = focusedAction()
|
|
|
if (index !== undefined) {
|
|
if (index !== undefined) {
|
|
@@ -265,6 +271,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function moveAction(direction: 1 | -1) {
|
|
function moveAction(direction: 1 | -1) {
|
|
|
|
|
+ if (props.locked) return
|
|
|
const total = actionItems().length
|
|
const total = actionItems().length
|
|
|
if (total === 0) return
|
|
if (total === 0) return
|
|
|
setFocusedAction((index) => {
|
|
setFocusedAction((index) => {
|
|
@@ -275,7 +282,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
useBindings(() => {
|
|
useBindings(() => {
|
|
|
- const enabledActions = actions().filter((item) => !item.disabled)
|
|
|
|
|
|
|
+ const visible = shownActions()
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
commands: [
|
|
commands: [
|
|
@@ -320,6 +327,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
title: "First item",
|
|
title: "First item",
|
|
|
category: "Dialog",
|
|
category: "Dialog",
|
|
|
run() {
|
|
run() {
|
|
|
|
|
+ if (props.locked) return
|
|
|
setStore("input", "keyboard")
|
|
setStore("input", "keyboard")
|
|
|
moveTo(0)
|
|
moveTo(0)
|
|
|
},
|
|
},
|
|
@@ -329,6 +337,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
title: "Last item",
|
|
title: "Last item",
|
|
|
category: "Dialog",
|
|
category: "Dialog",
|
|
|
run() {
|
|
run() {
|
|
|
|
|
+ if (props.locked) return
|
|
|
setStore("input", "keyboard")
|
|
setStore("input", "keyboard")
|
|
|
moveTo(flat().length - 1)
|
|
moveTo(flat().length - 1)
|
|
|
},
|
|
},
|
|
@@ -339,11 +348,13 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
category: "Dialog",
|
|
category: "Dialog",
|
|
|
run: submit,
|
|
run: submit,
|
|
|
},
|
|
},
|
|
|
- ...enabledActions.map((item) => ({
|
|
|
|
|
|
|
+ ...visible.map((item) => ({
|
|
|
name: item.command,
|
|
name: item.command,
|
|
|
title: item.title,
|
|
title: item.title,
|
|
|
category: "Dialog",
|
|
category: "Dialog",
|
|
|
run() {
|
|
run() {
|
|
|
|
|
+ if (props.locked) return
|
|
|
|
|
+ if (isActionDisabled(item)) return
|
|
|
setStore("input", "keyboard")
|
|
setStore("input", "keyboard")
|
|
|
const option = selected()
|
|
const option = selected()
|
|
|
if (!option) return
|
|
if (!option) return
|
|
@@ -361,8 +372,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
"dialog.select.end",
|
|
"dialog.select.end",
|
|
|
"dialog.select.submit",
|
|
"dialog.select.submit",
|
|
|
]),
|
|
]),
|
|
|
- ...enabledActions.flatMap((item) => tuiConfig.keybinds.get(item.command)),
|
|
|
|
|
- ...(enabledActions.length
|
|
|
|
|
|
|
+ ...visible.flatMap((item) => tuiConfig.keybinds.get(item.command)),
|
|
|
|
|
+ ...(visible.length
|
|
|
? [
|
|
? [
|
|
|
{
|
|
{
|
|
|
key: "tab",
|
|
key: "tab",
|
|
@@ -380,7 +391,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
: []),
|
|
: []),
|
|
|
...(props.bindings ?? []).filter((binding) => {
|
|
...(props.bindings ?? []).filter((binding) => {
|
|
|
if (typeof binding.cmd !== "string") return true
|
|
if (typeof binding.cmd !== "string") return true
|
|
|
- return enabledActions.some((item) => item.command === binding.cmd)
|
|
|
|
|
|
|
+ return visible.some((item) => item.command === binding.cmd)
|
|
|
}),
|
|
}),
|
|
|
],
|
|
],
|
|
|
}
|
|
}
|
|
@@ -408,7 +419,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
const right = createMemo(() => visibleActions().filter((item) => item.side === "right"))
|
|
const right = createMemo(() => visibleActions().filter((item) => item.side === "right"))
|
|
|
|
|
|
|
|
function triggerAction(item: VisibleAction | undefined) {
|
|
function triggerAction(item: VisibleAction | undefined) {
|
|
|
- if (!item || !isActionItem(item)) return
|
|
|
|
|
|
|
+ if (props.locked) return
|
|
|
|
|
+ if (!item || !isActionItem(item) || isActionDisabled(item)) return
|
|
|
setStore("input", "keyboard")
|
|
setStore("input", "keyboard")
|
|
|
const option = selected()
|
|
const option = selected()
|
|
|
if (!option) return
|
|
if (!option) return
|
|
@@ -419,7 +431,12 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
return "onTrigger" in item
|
|
return "onTrigger" in item
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function isActionDisabled(item: Action) {
|
|
|
|
|
+ return typeof item.disabled === "function" ? item.disabled(selected()) : item.disabled
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function isActionFocused(item: VisibleAction) {
|
|
function isActionFocused(item: VisibleAction) {
|
|
|
|
|
+ if (props.locked) return false
|
|
|
if (!isActionItem(item)) return false
|
|
if (!isActionItem(item)) return false
|
|
|
return actionItems().indexOf(item) === focusedAction()
|
|
return actionItems().indexOf(item) === focusedAction()
|
|
|
}
|
|
}
|
|
@@ -434,19 +451,24 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
<span style={{ fg: theme.textMuted }}>{action.item.label}</span>
|
|
<span style={{ fg: theme.textMuted }}>{action.item.label}</span>
|
|
|
</text>
|
|
</text>
|
|
|
)
|
|
)
|
|
|
- const active = createMemo(() => isActionFocused(action.item))
|
|
|
|
|
|
|
+ const item = action.item
|
|
|
|
|
+ const active = createMemo(() => isActionFocused(item))
|
|
|
|
|
+ const disabled = createMemo(() => isActionDisabled(item))
|
|
|
const fg = selectedForeground(theme)
|
|
const fg = selectedForeground(theme)
|
|
|
return (
|
|
return (
|
|
|
<box
|
|
<box
|
|
|
flexDirection="row"
|
|
flexDirection="row"
|
|
|
paddingRight={1}
|
|
paddingRight={1}
|
|
|
backgroundColor={active() ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
|
backgroundColor={active() ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
|
|
- onMouseUp={() => triggerAction(action.item)}
|
|
|
|
|
|
|
+ onMouseUp={() => triggerAction(item)}
|
|
|
>
|
|
>
|
|
|
- <text fg={active() ? fg : theme.text} attributes={active() ? TextAttributes.BOLD : undefined}>
|
|
|
|
|
- {action.item.title}
|
|
|
|
|
|
|
+ <text
|
|
|
|
|
+ fg={disabled() ? theme.textMuted : active() ? fg : theme.text}
|
|
|
|
|
+ attributes={active() ? TextAttributes.BOLD : undefined}
|
|
|
|
|
+ >
|
|
|
|
|
+ {item.title}
|
|
|
</text>
|
|
</text>
|
|
|
- <text fg={active() ? fg : theme.textMuted}> {action.item.label}</text>
|
|
|
|
|
|
|
+ <text fg={disabled() ? theme.textMuted : active() ? fg : theme.textMuted}> {item.label}</text>
|
|
|
</box>
|
|
</box>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
@@ -455,9 +477,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
<box gap={1} paddingBottom={1} flexGrow={1}>
|
|
<box gap={1} paddingBottom={1} flexGrow={1}>
|
|
|
<box paddingLeft={4} paddingRight={4}>
|
|
<box paddingLeft={4} paddingRight={4}>
|
|
|
<box flexDirection="row" justifyContent="space-between">
|
|
<box flexDirection="row" justifyContent="space-between">
|
|
|
- <text fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
|
|
|
- {props.title}
|
|
|
|
|
- </text>
|
|
|
|
|
|
|
+ {props.titleView ?? (
|
|
|
|
|
+ <text fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
|
|
|
+ {props.title}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ )}
|
|
|
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
|
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
|
|
esc
|
|
esc
|
|
|
</text>
|
|
</text>
|
|
@@ -466,6 +490,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
<box paddingTop={1}>
|
|
<box paddingTop={1}>
|
|
|
<input
|
|
<input
|
|
|
onInput={(e) => {
|
|
onInput={(e) => {
|
|
|
|
|
+ if (props.locked) return
|
|
|
batch(() => {
|
|
batch(() => {
|
|
|
setStore("filter", e)
|
|
setStore("filter", e)
|
|
|
props.onFilter?.(e)
|
|
props.onFilter?.(e)
|
|
@@ -525,7 +550,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
</Show>
|
|
</Show>
|
|
|
<For each={options}>
|
|
<For each={options}>
|
|
|
{(option) => {
|
|
{(option) => {
|
|
|
- const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
|
|
|
|
|
|
|
+ const active = createMemo(() => !props.locked && isDeepEqual(option.value, selected()?.value))
|
|
|
const current = createMemo(() => isDeepEqual(option.value, props.current))
|
|
const current = createMemo(() => isDeepEqual(option.value, props.current))
|
|
|
return (
|
|
return (
|
|
|
<box
|
|
<box
|
|
@@ -533,20 +558,24 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
|
flexDirection="column"
|
|
flexDirection="column"
|
|
|
position="relative"
|
|
position="relative"
|
|
|
onMouseMove={() => {
|
|
onMouseMove={() => {
|
|
|
|
|
+ if (props.locked) return
|
|
|
setStore("input", "mouse")
|
|
setStore("input", "mouse")
|
|
|
setFocusedAction(undefined)
|
|
setFocusedAction(undefined)
|
|
|
}}
|
|
}}
|
|
|
onMouseUp={() => {
|
|
onMouseUp={() => {
|
|
|
|
|
+ if (props.locked) return
|
|
|
option.onSelect?.(dialog)
|
|
option.onSelect?.(dialog)
|
|
|
props.onSelect?.(option)
|
|
props.onSelect?.(option)
|
|
|
}}
|
|
}}
|
|
|
onMouseOver={() => {
|
|
onMouseOver={() => {
|
|
|
|
|
+ if (props.locked) return
|
|
|
if (store.input !== "mouse") return
|
|
if (store.input !== "mouse") return
|
|
|
const index = flat().findIndex((x) => isDeepEqual(x.value, option.value))
|
|
const index = flat().findIndex((x) => isDeepEqual(x.value, option.value))
|
|
|
if (index === -1) return
|
|
if (index === -1) return
|
|
|
moveTo(index)
|
|
moveTo(index)
|
|
|
}}
|
|
}}
|
|
|
onMouseDown={() => {
|
|
onMouseDown={() => {
|
|
|
|
|
+ if (props.locked) return
|
|
|
const index = flat().findIndex((x) => isDeepEqual(x.value, option.value))
|
|
const index = flat().findIndex((x) => isDeepEqual(x.value, option.value))
|
|
|
if (index === -1) return
|
|
if (index === -1) return
|
|
|
moveTo(index)
|
|
moveTo(index)
|