feat(file-tree): use anchor tags for better navigation

Convert div elements to anchor tags in FileTree and HeadingNode to
enable native browser features like middle-click to open in new tabs.
The implementation preserves SPA navigation for left-clicks while
ensuring search parameters are preserved in the href.
This commit is contained in:
hypercross 2026-07-12 10:46:35 +08:00
parent 40f2190307
commit 722a8110e6
1 changed files with 23 additions and 9 deletions

View File

@ -1,4 +1,5 @@
import { Component, createMemo, createSignal, Show } from "solid-js"; import { Component, createMemo, createSignal, Show } from "solid-js";
import { useLocation } from "@solidjs/router";
import { type FileNode, type TocNode } from "../data-loader"; import { type FileNode, type TocNode } from "../data-loader";
import { useNavigateWithParams } from "./useNavigateWithParams"; import { useNavigateWithParams } from "./useNavigateWithParams";
import { useScrollContainer } from "../App"; import { useScrollContainer } from "../App";
@ -24,6 +25,7 @@ export const FileTreeNode: Component<{
isHidden?: (node: FileNode) => boolean; isHidden?: (node: FileNode) => boolean;
}> = (props) => { }> = (props) => {
const navigate = useNavigateWithParams(); const navigate = useNavigateWithParams();
const location = useLocation();
const isDir = !!props.node.children; const isDir = !!props.node.children;
const isActive = createMemo(() => props.currentPath === props.node.path); const isActive = createMemo(() => props.currentPath === props.node.path);
// 默认收起,除非当前文件在该文件夹内 // 默认收起,除非当前文件在该文件夹内
@ -31,21 +33,28 @@ export const FileTreeNode: Component<{
isDir && isPathInDir(props.currentPath, props.node.path), isDir && isPathInDir(props.currentPath, props.node.path),
); );
const handleClick = () => { const href = () => props.node.path + location.search;
const handleClick = (e: MouseEvent) => {
if (isDir) { if (isDir) {
e.preventDefault();
setIsExpanded(!isExpanded()); setIsExpanded(!isExpanded());
} else { } else if (e.button === 0) {
// Left-click: use SPA navigation
e.preventDefault();
navigate(props.node.path); navigate(props.node.path);
props.onClose(); props.onClose();
} }
// Middle-click / right-click: let the browser handle the <a> natively
}; };
const indent = props.depth * 12; const indent = props.depth * 12;
return ( return (
<div class={props.isHidden?.(props.node) === true ? "hidden" : ""}> <div class={props.isHidden?.(props.node) === true ? "hidden" : ""}>
<div <a
class={`flex items-center py-1 px-2 cursor-pointer hover:bg-gray-100 rounded ${ href={href()}
class={`flex items-center py-1 px-2 cursor-pointer hover:bg-gray-100 rounded no-underline ${
isActive() ? "bg-blue-50 text-blue-700" : "text-gray-700" isActive() ? "bg-blue-50 text-blue-700" : "text-gray-700"
}`} }`}
style={{ "padding-left": `${indent + 8}px` }} style={{ "padding-left": `${indent + 8}px` }}
@ -58,7 +67,7 @@ export const FileTreeNode: Component<{
<span class="mr-1 text-gray-400">📄</span> <span class="mr-1 text-gray-400">📄</span>
</Show> </Show>
<span class="text-sm truncate">{props.node.name}</span> <span class="text-sm truncate">{props.node.name}</span>
</div> </a>
<Show when={isDir && isExpanded() && props.node.children}> <Show when={isDir && isExpanded() && props.node.children}>
<div> <div>
{props.node.children!.map((child) => ( {props.node.children!.map((child) => (
@ -87,8 +96,9 @@ export const HeadingNode: Component<{
isHidden?: (node: TocNode) => boolean; isHidden?: (node: TocNode) => boolean;
}> = (props) => { }> = (props) => {
const navigate = useNavigateWithParams(); const navigate = useNavigateWithParams();
const location = useLocation();
const anchor = props.node.id || ""; const anchor = props.node.id || "";
const href = `${props.basePath}#${anchor}`; const href = () => `${props.basePath}${location.search}#${anchor}`;
const hasChildren = !!props.node.children; const hasChildren = !!props.node.children;
// 默认收起,除非当前锚点在该节点内 // 默认收起,除非当前锚点在该节点内
const [isExpanded, setIsExpanded] = createSignal(props.depth <= 0); const [isExpanded, setIsExpanded] = createSignal(props.depth <= 0);
@ -102,8 +112,12 @@ export const HeadingNode: Component<{
const scrollContainer = useScrollContainer(); const scrollContainer = useScrollContainer();
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
e.preventDefault(); if (e.button === 0) {
navigate(href); // Left-click: use SPA navigation
e.preventDefault();
navigate(href());
}
// Middle-click / right-click: let the browser handle the <a> natively
// 滚动到目标元素,考虑导航栏高度偏移 // 滚动到目标元素,考虑导航栏高度偏移
requestAnimationFrame(() => { requestAnimationFrame(() => {
const element = document.getElementById(anchor); const element = document.getElementById(anchor);
@ -141,7 +155,7 @@ export const HeadingNode: Component<{
</span> </span>
</span> </span>
<a <a
href={href} href={href()}
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" 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"
onClick={handleClick} onClick={handleClick}
> >