Astro目录生成-TableOfContents
Astro提供的md文件的标题headings
根据从每个Post的属性中获取到的headings,我们可以方便的进行渲染目录,以及目录组件。
interface Props {
entry: CollectionEntry<'blog'>;
}
const { entry } = Astro.props;
const { headings } = await entry.render();
const { title, description, pubDate, author, tags, categories } = entry.data;
首先创建一个Toc组件
在这个组件中根据传进的headings构造我们的目录,编写一个generateToc进行链接数组对象的生成
interface Props {
headings: {
depth: number;
slug: string;
text: string;
}[];
title: string;
}
const { headings, title } = Astro.props;
const generateToc = (headings: any[]) => {
const toc: any[] = [];
const parentHeadings = new Map();
let minDepth = Number.MAX_SAFE_INTEGER;
for (const heading of headings) {
if (heading.depth < minDepth) {
minDepth = heading.depth;
}
}
for (const heading of headings) {
const sub: any[] = [];
const { depth } = heading;
const currentHeading = { ...heading, sub };
parentHeadings.set(depth, currentHeading);
if (depth === minDepth || depth === 2) {
toc.push(currentHeading);
} else {
const parentHeading = parentHeadings.get(depth - 1);
parentHeading ? parentHeading.sub.push(currentHeading) : toc.push(currentHeading);
}
}
return toc;
};
---
{
headings.length > 0 && (
<details class='mt-5' open>
<summary class='mb-5'>{title}</summary>
<ul id='tocList' class='max-h-[50vh] h-full overflow-y-auto'>
{generateToc(headings).map((heading) => (
<TocLink heading={heading} />
))}
</ul>
</details>
)
}
创建目录链接组件TocLink
这个组件需要进行递归渲染我们的链接。主要是参考<Astro.self heading={subheading} />
---
import ActiveLink from './ActiveLink.astro';
const { heading } = Astro.props;
---
<li class='toc-item'>
<ActiveLink href={`#${heading.slug}`} class='toc-link'>{heading.text}</ActiveLink>
{
heading.sub && heading.sub.length > 0 && (
<ul class='ml-4'>
{heading.sub.map((subheading) => (
<Astro.self heading={subheading} />
))}
</ul>
)
}
</li>
需要编写一些js帮助我们进行定位和滚动
采用IntersectionObserver进行监听我们的目录标题,出现时我们高亮对应的目录链接,同时我们需要做一些防抖处理
<script>
// JavaScript 逻辑
function scrollToAnchor(link) {
const href = link.getAttribute('href');
const targetElement = document.getElementById(href.substring(1));
if (targetElement) {
targetElement.scrollIntoView(true);
}
}
function setupTocLinkObserver() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const link = document.querySelector(`.toc-link[href="#${entry.target.id}"]`);
if (link) {
document.querySelectorAll('.toc-link').forEach((item) => {
item.classList.remove('active');
item.parentElement?.classList.remove('active');
});
link.classList.add('active');
link.parentElement?.classList.add('active');
}
}
});
debouncedHandleScrollEnd();
},
{
root: document, // 相对于视口观察
rootMargin: '0px 0px -75% 0px',
threshold: 1 // 元素与视口交叉的阈值
}
);
// 为每个锚点设置观察器
document.querySelectorAll('.toc-link').forEach((link) => {
const href = link.getAttribute('href') || '';
const targetElement = document.getElementById(href.substring(1));
targetElement && observer.observe(targetElement);
});
}
function setScrollTo() {
const activeLinks = document.querySelectorAll('.toc-link.active');
const activeLastLink = activeLinks[activeLinks.length - 1] as HTMLElement;
const tocList = document.getElementById('tocList');
if (
activeLastLink &&
tocList &&
tocList.scrollHeight > tocList.offsetHeight &&
activeLastLink.classList.contains('active')
) {
// 获取所有高亮的目录项
const newtop =
activeLastLink.offsetTop -
tocList.clientHeight / 2 -
tocList.offsetTop -
activeLastLink.offsetHeight / 2;
// console.log(activeLastLink.offsetTop);
// console.log(newtop);
// 确保高亮的目录项在滚动区域内部是可见的
tocList.scrollTo({
top: newtop < 0 ? 0 : newtop,
behavior: 'smooth'
});
}
}
function debounce(func, delay) {
let debounceTimer;
return function (...args) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(this, args), delay);
};
}
const debouncedHandleScrollEnd = debounce(setScrollTo, 500);
const debouncedHandleScrollToAnchor = debounce(scrollToAnchor, 50);
document.addEventListener('astro:page-load', function () {
setupTocLinkObserver();
document.querySelectorAll('.toc-link').forEach((link) =>
link.addEventListener('click', (event) => {
event.preventDefault(); // 阻止默认的点击行为,避免直接跳转到锚点
// scrollToAnchor(link);
debouncedHandleScrollToAnchor(link);
})
);
});
</script>
完整的代码示例可以看本站点,以及实现效果,目前还有部分的地方需要待改进。