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:
parent
40f2190307
commit
722a8110e6
|
|
@ -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) => {
|
||||||
|
if (e.button === 0) {
|
||||||
|
// Left-click: use SPA navigation
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(href);
|
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}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue