import { useCallback, useEffect, useState } from "react"; import { Link } from "@remix-run/react"; import { cx } from "~/cva.config"; type TableOfContentsType = { id: string; title: string; level: number; children: Omit[]; }; function useTableOfContents(tableOfContents: TableOfContentsType[]) { let [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id); let getHeadings = useCallback((tableOfContents: TableOfContentsType[]) => { return tableOfContents .flatMap((node: TableOfContentsType) => [ node.id, ...node.children.map(child => child.id), ]) .map((id: string) => { let el = document.getElementById(id); if (!el) return null; let style = window.getComputedStyle(el); let scrollMt = parseFloat(style.scrollMarginTop); let top = window.scrollY + el.getBoundingClientRect().top - scrollMt; return { id, top }; }) .filter(x => x) as { id: string; top: number }[]; }, []); useEffect(() => { if (tableOfContents.length === 0) return; let headings = getHeadings(tableOfContents); function onScroll() { let top = window.scrollY; let current = headings[0].id; for (let heading of headings) { if (top >= heading.top) { current = heading?.id; } else { break; } } setCurrentSection(current); } window.addEventListener("scroll", onScroll, { passive: true }); onScroll(); return () => { window.removeEventListener("scroll", onScroll); }; }, [getHeadings, tableOfContents]); return currentSection; } export default function TableOfContents({ tableOfContents, }: { tableOfContents: TableOfContentsType[]; }) { let currentSection = useTableOfContents(tableOfContents); const isActive = (section: { id: string; children?: Array<{ id: string }> }) => { if (section.id === currentSection) return true; if (!section.children) return false; return section.children.findIndex(isActive) > -1; }; return ( ); }