ttrpg-tools/src/components/FileTree.tsx

141 lines
4.2 KiB
TypeScript
Raw Normal View History

2026-02-26 15:16:13 +08:00
import { Component, createMemo, createSignal, Show } from "solid-js";
2026-02-26 15:04:17 +08:00
import { type FileNode, type TocNode } from "../data-loader";
import { useNavigate } from "@solidjs/router";
2026-02-26 16:35:57 +08:00
/**
*
*/
function isPathInDir(currentPath: string, dirPath: string): boolean {
// 确保 dirPath 以 / 结尾,用于前缀匹配
const dirPathPrefix = dirPath.endsWith('/') ? dirPath : dirPath + '/';
return currentPath.startsWith(dirPathPrefix);
}
2026-02-26 15:04:17 +08:00
/**
*
*/
export const FileTreeNode: Component<{
node: FileNode;
currentPath: string;
pathHeadings: Record<string, TocNode[]>;
depth: number;
onClose: () => void;
}> = (props) => {
const navigate = useNavigate();
const isDir = !!props.node.children;
2026-02-26 15:16:13 +08:00
const isActive = createMemo(() => props.currentPath === props.node.path);
2026-02-26 16:35:57 +08:00
// 默认收起,除非当前文件在该文件夹内
const [isExpanded, setIsExpanded] = createSignal(isDir && isPathInDir(props.currentPath, props.node.path));
2026-02-26 15:04:17 +08:00
const handleClick = () => {
if (isDir) {
setIsExpanded(!isExpanded());
} else {
navigate(props.node.path);
props.onClose();
}
};
const indent = props.depth * 12;
return (
<div>
<div
class={`flex items-center py-1 px-2 cursor-pointer hover:bg-gray-100 rounded ${
2026-02-26 15:16:13 +08:00
isActive() ? "bg-blue-50 text-blue-700" : "text-gray-700"
2026-02-26 15:04:17 +08:00
}`}
style={{ "padding-left": `${indent + 8}px` }}
onClick={handleClick}
>
<Show when={isDir}>
<span class="mr-1 text-gray-400">{isExpanded() ? "📂" : "📁"}</span>
</Show>
<Show when={!isDir}>
<span class="mr-1 text-gray-400">📄</span>
</Show>
<span class="text-sm truncate">{props.node.name}</span>
</div>
<Show when={isDir && isExpanded() && props.node.children}>
<div>
{props.node.children!.map((child) => (
<FileTreeNode
node={child}
currentPath={props.currentPath}
pathHeadings={props.pathHeadings}
depth={props.depth + 1}
onClose={props.onClose}
/>
))}
</div>
</Show>
</div>
);
};
/**
*
*/
export const HeadingNode: Component<{
node: TocNode;
basePath: string;
depth: number;
}> = (props) => {
const navigate = useNavigate();
2026-03-19 11:22:12 +08:00
const anchor = props.node.id || "";
2026-02-26 15:04:17 +08:00
const href = `${props.basePath}#${anchor}`;
2026-03-25 10:26:06 +08:00
const hasChildren = !!props.node.children;
// 默认收起,除非当前锚点在该节点内
2026-03-25 10:49:15 +08:00
const [isExpanded, setIsExpanded] = createSignal(props.depth <= 0);
2026-02-26 15:04:17 +08:00
2026-03-25 10:49:15 +08:00
const handleExpand = (e: MouseEvent) => {
2026-02-26 15:04:17 +08:00
e.preventDefault();
2026-03-25 10:26:06 +08:00
if (hasChildren) {
setIsExpanded(!isExpanded());
}
2026-03-25 10:49:15 +08:00
};
const handleClick = (e: MouseEvent) => {
2026-02-26 15:04:17 +08:00
navigate(href);
2026-03-02 13:39:36 +08:00
// 滚动到目标元素,考虑导航栏高度偏移
2026-03-02 11:03:51 +08:00
requestAnimationFrame(() => {
const element = document.getElementById(anchor);
if (element) {
2026-03-02 13:39:36 +08:00
const navBarHeight = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = window.scrollY + elementPosition - navBarHeight;
2026-03-25 10:49:15 +08:00
window.scrollTo({ top: offsetPosition, behavior: "instant" });
2026-03-02 11:03:51 +08:00
}
});
2026-02-26 15:04:17 +08:00
};
const indent = props.depth * 12;
return (
<div>
2026-03-25 10:49:15 +08:00
<span class={`${hasChildren ? "" : "invisible" } ${isExpanded() ? "rotate-90" : ""} inline-block transition-transform cursor-pointer mr-1 text-gray-400 text-xs w-3 flex-shrink-0`}
style={{ "margin-left": `${indent + 8}px` }}
onClick={handleExpand}
>
</span>
2026-02-26 15:04:17 +08:00
<a
href={href}
2026-03-25 10:49:15 +08:00
class="inline-flex items-center py-0.5 px-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded truncate cursor-pointer"
2026-02-26 15:04:17 +08:00
onClick={handleClick}
>
2026-03-25 10:26:06 +08:00
<span class="truncate">{props.node.title}</span>
2026-02-26 15:04:17 +08:00
</a>
2026-03-25 10:26:06 +08:00
<Show when={hasChildren && isExpanded()}>
2026-02-26 15:04:17 +08:00
<div>
{props.node.children!.map((child) => (
<HeadingNode
node={child}
basePath={props.basePath}
depth={props.depth + 1}
/>
))}
</div>
</Show>
</div>
);
};