ttrpg-tools/src/components/md-commander/index.tsx

147 lines
5.1 KiB
TypeScript
Raw Normal View History

2026-02-28 16:28:07 +08:00
import { customElement, noShadowDOM } from "solid-element";
2026-03-01 12:41:03 +08:00
import { onMount, onCleanup, Show, createEffect, on } from "solid-js";
import { useCommander } from "./hooks";
2026-02-28 16:28:07 +08:00
import { CommanderInput } from "./CommanderInput";
import { CommanderEntries } from "./CommanderEntries";
2026-03-01 09:36:09 +08:00
import { TrackerView } from "./TrackerView";
import { TabBar } from "./TabBar";
2026-03-01 11:52:09 +08:00
import type { MdCommanderProps } from "./types";
2026-03-01 12:49:06 +08:00
import { loadElementSrc } from "../utils/path";
import { initializeCommands, loadCommandTemplatesFromCSV } from "./stores";
2026-02-28 16:28:07 +08:00
customElement<MdCommanderProps>(
"md-commander",
2026-03-01 11:52:09 +08:00
{ placeholder: "", class: "", height: "", commandTemplates: "" },
2026-02-28 16:28:07 +08:00
(props, { element }) => {
noShadowDOM();
2026-03-01 12:33:55 +08:00
const { articlePath, rawSrc } = loadElementSrc(element as any);
2026-03-01 12:41:03 +08:00
// 初始化命令并注册到 store
const commands = initializeCommands(props.commands);
const commander = useCommander(commands);
// 加载 CSV 模板
2026-03-01 12:49:06 +08:00
createEffect( async () => {
if (!rawSrc || !rawSrc) return;
await loadCommandTemplatesFromCSV(rawSrc, articlePath);
2026-03-01 12:41:03 +08:00
// 更新 commander 中的命令(从 store 获取)
2026-03-01 12:49:06 +08:00
const {getCommands} = await import("./stores/commandsStore");
2026-03-01 12:41:03 +08:00
commander.setCommands(getCommands());
2026-03-01 12:49:06 +08:00
});
2026-02-28 16:28:07 +08:00
const handleKeyDown = (e: KeyboardEvent) => {
if (commander.showCompletions() && commander.completions().length > 0) {
if (e.key === "ArrowDown") {
e.preventDefault();
commander.setSelectedCompletion(
(prev) => (prev + 1) % commander.completions().length,
);
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
commander.setSelectedCompletion(
(prev) => (prev - 1 + commander.completions().length) % commander.completions().length,
);
return;
}
if (e.key === "Tab") {
e.preventDefault();
commander.acceptCompletion();
return;
}
if (e.key === "Escape") {
2026-03-01 10:26:37 +08:00
commander.setShowCompletions(false);
2026-02-28 16:28:07 +08:00
return;
}
}
2026-02-28 17:15:41 +08:00
if (e.key === "ArrowUp" && !commander.showCompletions()) {
e.preventDefault();
commander.navigateHistory("up");
return;
}
if (e.key === "ArrowDown" && !commander.showCompletions()) {
e.preventDefault();
commander.navigateHistory("down");
return;
}
2026-02-28 16:28:07 +08:00
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
commander.handleCommand();
}
};
const heightStyle = () => props.height || "400px";
onMount(() => {
const handleClickOutside = (e: MouseEvent) => {
if (!element?.contains(e.target as Node)) {
commander.setShowCompletions(false);
}
};
document.addEventListener("click", handleClickOutside);
onCleanup(() => document.removeEventListener("click", handleClickOutside));
});
return (
<div
2026-02-28 16:49:02 +08:00
class={`md-commander flex flex-col border border-gray-300 rounded-lg overflow-hidden ${props.class || ""}`}
2026-02-28 16:28:07 +08:00
style={{ height: heightStyle() }}
>
2026-03-01 12:33:55 +08:00
<TabBar mode={commander.viewMode} onModeChange={commander.setViewMode} />
2026-02-28 16:30:58 +08:00
2026-03-01 09:36:09 +08:00
<Show
when={commander.viewMode() === "history"}
fallback={
<TrackerView
items={commander.trackerItems}
2026-03-01 10:01:53 +08:00
onEditAttribute={(index, attrName, attr) =>
2026-03-01 12:33:55 +08:00
commander.updateTrackerAttributeByIndex(index, attrName, attr)
2026-03-01 09:36:09 +08:00
}
2026-03-01 10:33:55 +08:00
onClassesChange={(index, classes) =>
commander.updateTrackerClassesByIndex(index, classes)
}
2026-03-01 12:33:55 +08:00
onRemoveClass={(index, className) =>
commander.removeTrackerItemClassByIndex(index, className)
}
2026-03-01 10:01:53 +08:00
onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")}
onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")}
onRemove={(index) => commander.removeTrackerItemByIndex(index)}
2026-03-01 09:36:09 +08:00
/>
}
>
<CommanderEntries
entries={commander.entries}
onCommandClick={(cmd) => commander.setInputValue(cmd)}
/>
</Show>
2026-02-28 16:49:02 +08:00
<div class="relative border-t border-gray-300">
2026-02-28 16:28:07 +08:00
<CommanderInput
placeholder={props.placeholder}
inputValue={commander.inputValue}
onInput={(e) => {
commander.setInputValue((e.target as HTMLInputElement).value);
commander.updateCompletions();
}}
onKeyDown={handleKeyDown}
onFocus={() => {
commander.setIsFocused(true);
commander.updateCompletions();
}}
onBlur={() => commander.setIsFocused(false)}
onSubmit={commander.handleCommand}
showCompletions={commander.showCompletions}
completions={commander.completions}
selectedCompletion={commander.selectedCompletion}
onSelectCompletion={commander.setSelectedCompletion}
onAcceptCompletion={commander.acceptCompletion}
/>
</div>
</div>
);
},
);