feat: 文章添加toc
This commit is contained in:
172
src/components/Post/Toc.astro
Normal file
172
src/components/Post/Toc.astro
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
import { type TocNode } from "@/lib/processHtml";
|
||||
|
||||
interface Props {
|
||||
toc: TocNode[];
|
||||
}
|
||||
|
||||
const { toc } = Astro.props;
|
||||
const lowLevel = Math.min(...toc.map((v) => v.level));
|
||||
---
|
||||
|
||||
<div class="toc-mount">
|
||||
<div class="toc">
|
||||
{
|
||||
toc.map((item) => {
|
||||
return (
|
||||
<a
|
||||
href={`#${item.id}`}
|
||||
class="toc-item"
|
||||
style={{ paddingLeft: `${(item.level - lowLevel) * 8}px` }}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toc-mount {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
height: 100%;
|
||||
right: -320px;
|
||||
width: 280px;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.toc {
|
||||
position: sticky;
|
||||
top: 60px;
|
||||
height: calc(100vh - 120px);
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
border-left: var(--border-secondary);
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.toc-item {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.toc-item:hover {
|
||||
color: var(--text-title-hover);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.toc-item.is-active {
|
||||
position: relative;
|
||||
color: var(--text-title-hover);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.toc-item.is-active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -25px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 2px;
|
||||
height: 18px;
|
||||
border-radius: 2px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
@media (max-width: 1250px) {
|
||||
.toc-mount{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const links = Array.from(
|
||||
document.querySelectorAll<HTMLAnchorElement>(".toc-item"),
|
||||
);
|
||||
|
||||
const headings = links
|
||||
.map((link) => {
|
||||
const id = link.getAttribute("href")?.slice(1);
|
||||
return id ? { link, heading: document.getElementById(id) } : null;
|
||||
})
|
||||
.filter((x): x is { link: HTMLAnchorElement; heading: HTMLElement } =>
|
||||
Boolean(x?.heading),
|
||||
);
|
||||
|
||||
if (headings.length > 0) {
|
||||
const toc = document.querySelector<HTMLElement>(".toc");
|
||||
const ACTIVE_OFFSET = 100;
|
||||
|
||||
let currentId: string | null = null;
|
||||
let raf = 0;
|
||||
|
||||
const scrollActiveIntoView = (link: HTMLAnchorElement) => {
|
||||
if (!toc) return;
|
||||
const containerTop = toc.getBoundingClientRect().top;
|
||||
const linkTop =
|
||||
link.getBoundingClientRect().top - containerTop + toc.scrollTop;
|
||||
const linkBottom =
|
||||
link.getBoundingClientRect().bottom - containerTop + toc.scrollTop;
|
||||
if (linkTop < toc.scrollTop) {
|
||||
toc.scrollTop = linkTop;
|
||||
} else if (linkBottom > toc.scrollTop + toc.clientHeight) {
|
||||
toc.scrollTop = linkBottom - toc.clientHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const setActive = (id: string | null) => {
|
||||
if (id === currentId) return;
|
||||
currentId = id;
|
||||
for (const { link, heading } of headings) {
|
||||
const active = heading.id === id;
|
||||
link.classList.toggle("is-active", active);
|
||||
if (active) {
|
||||
link.setAttribute("aria-current", "location");
|
||||
scrollActiveIntoView(link);
|
||||
} else {
|
||||
link.removeAttribute("aria-current");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const update = () => {
|
||||
raf = 0;
|
||||
|
||||
const doc = document.documentElement;
|
||||
const atBottom =
|
||||
window.scrollY + window.innerHeight >= doc.scrollHeight - 4;
|
||||
|
||||
let activeId: string | null = null;
|
||||
|
||||
if (atBottom) {
|
||||
activeId = headings[headings.length - 1].heading.id;
|
||||
} else {
|
||||
for (const { heading } of headings) {
|
||||
if (heading.getBoundingClientRect().top <= ACTIVE_OFFSET) {
|
||||
activeId = heading.id;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setActive(activeId);
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
if (raf) return;
|
||||
raf = requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
window.addEventListener("resize", onScroll, { passive: true });
|
||||
update();
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user