103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import {type Component, For, Show, createEffect, on} from "solid-js";
|
||
import type { CompletionItem } from "./types";
|
||
|
||
export interface CommanderInputProps {
|
||
placeholder?: string;
|
||
inputValue: () => string;
|
||
onInput: (e: Event) => void;
|
||
onKeyDown: (e: KeyboardEvent) => void;
|
||
onFocus: () => void;
|
||
onBlur: () => void;
|
||
onSubmit: () => void;
|
||
showCompletions: () => boolean;
|
||
completions: () => CompletionItem[];
|
||
selectedCompletion: () => number;
|
||
onSelectCompletion: (idx: number) => void;
|
||
onAcceptCompletion: () => void;
|
||
}
|
||
|
||
export const CommanderInput: Component<CommanderInputProps> = (props) => {
|
||
let containerRef: HTMLDivElement | undefined;
|
||
|
||
// 当选中项变化时自动滚动到可见区域
|
||
createEffect(
|
||
on(
|
||
() => props.selectedCompletion(),
|
||
() => {
|
||
if (containerRef) {
|
||
const selectedItem = containerRef.querySelector('[data-selected="true"]');
|
||
if (selectedItem) {
|
||
selectedItem.scrollIntoView({ block: "nearest" });
|
||
}
|
||
}
|
||
},
|
||
),
|
||
);
|
||
|
||
const maxVisible = 3;
|
||
|
||
return (
|
||
<div class="flex items-center border-b border-gray-300 bg-gray-50 px-3 py-2">
|
||
<span class="text-gray-500 mr-2">❯</span>
|
||
<input
|
||
type="text"
|
||
value={props.inputValue()}
|
||
onInput={props.onInput}
|
||
onKeyDown={props.onKeyDown}
|
||
onFocus={props.onFocus}
|
||
onBlur={props.onBlur}
|
||
placeholder={props.placeholder || "输入命令..."}
|
||
class="flex-1 bg-transparent outline-none text-gray-800"
|
||
/>
|
||
<button
|
||
onClick={props.onSubmit}
|
||
class="ml-2 px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||
>
|
||
执行
|
||
</button>
|
||
|
||
{/* 自动补全下拉框 - 向上弹出 */}
|
||
<Show when={props.showCompletions() && props.completions().length > 0}>
|
||
<div
|
||
ref={containerRef}
|
||
class="absolute left-0 bottom-full w-full bg-white border border-gray-300 shadow-lg mb-1 overflow-auto"
|
||
style={{ "max-height": `${maxVisible * 40}px` }}
|
||
>
|
||
<For each={props.completions()}>{(comp, idx) => (
|
||
<div
|
||
data-selected={idx() === props.selectedCompletion()}
|
||
class={`px-3 py-2 cursor-pointer flex justify-between items-center ${
|
||
idx() === props.selectedCompletion()
|
||
? "bg-blue-100"
|
||
: "hover:bg-gray-100"
|
||
}`}
|
||
onClick={() => {
|
||
props.onSelectCompletion(idx());
|
||
props.onAcceptCompletion();
|
||
}}
|
||
>
|
||
<div class="flex items-center gap-2">
|
||
<span
|
||
class={`text-xs px-1 rounded ${
|
||
comp.type === "command"
|
||
? "bg-purple-100 text-purple-700"
|
||
: comp.type === "option"
|
||
? "bg-green-100 text-green-700"
|
||
: "bg-gray-100 text-gray-700"
|
||
}`}
|
||
>
|
||
{comp.type}
|
||
</span>
|
||
<span class="font-mono text-sm">{comp.label}</span>
|
||
</div>
|
||
<Show when={comp.description}>
|
||
<span class="text-xs text-gray-500">{comp.description}</span>
|
||
</Show>
|
||
</div>
|
||
)}</For>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
);
|
||
};
|