Astro目录生成-TableOfContents

发布于 #Astro

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>
  )
}

这个组件需要进行递归渲染我们的链接。主要是参考<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>

完整的代码示例可以看本站点,以及实现效果,目前还有部分的地方需要待改进。

相关文章

Astro实现分页

为需要分页的page创建对应文件 例如创建archives/[…page].astro 使用pag...

发布于 #Astro