src/components/TableOfContents.astro (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
--- import { Icon } from 'astro-icon/components' import TableOfContentsHeading from './TableOfContentsHeading.astro' export interface Heading { depth: number slug: string text: string subheadings: Heading[] } const { headings } = Astro.props const toc = buildToc(headings) function buildToc(headings: Heading[]): Heading[] { const toc: Heading[] = [] const stack: Heading[] = [] headings.forEach((h) => { const heading = { ...h, subheadings: [] } while (stack.length > 0 && stack[stack.length - 1].depth >= heading.depth) { stack.pop() } if (stack.length === 0) { toc.push(heading) } else { stack[stack.length - 1].subheadings.push(heading) } stack.push(heading) }) return toc } --- <details open class="group block rounded-xl border p-4 xl:hidden"> <summary class="flex cursor-pointer items-center justify-between text-xl font-semibold" > Table of Contents <Icon name="lucide:chevron-down" class="size-5 transition-transform group-open:rotate-180" /> </summary> <nav> <ul class="pt-3"> {toc.map((heading) => <TableOfContentsHeading heading={heading} />)} </ul> </nav> </details> <nav class="overflow-wrap-break-word sticky top-16 hidden h-0 w-[calc(50vw-50%-4rem)] translate-x-[calc(-100%-2em)] text-xs leading-4 xl:block" > <div class="mr-6 flex justify-end"> <ul class="mr-6 flex max-h-[calc(100vh-8rem)] flex-col justify-end gap-y-2 overflow-y-auto" id="toc-container" > <li> <h2 class="mb-2 text-lg font-semibold">Table of Contents</h2> </li> {toc.map((heading) => <TableOfContentsHeading heading={heading} />)} </ul> </div> </nav> <script> function setupToc() { const header = document.querySelector('header') const headerHeight = header ? header.offsetHeight : 0 const observer = new IntersectionObserver( (sections) => { sections.forEach((section) => { const heading = section.target.querySelector('h2, h3, h4, h5, h6') if (!heading) return const id = heading.getAttribute('id') const link = document.querySelector( `#toc-container li a[href="#${id}"]`, ) if (!link) return const addRemove = section.isIntersecting ? 'add' : 'remove' link.classList[addRemove]('text-foreground') }) }, { rootMargin: `-${headerHeight}px 0px 0px 0px`, }, ) const sections = document.querySelectorAll('.prose section') sections.forEach((section) => { observer.observe(section) }) } document.addEventListener('astro:page-load', setupToc) document.addEventListener('astro:after-swap', setupToc) </script> |