import { useState, useEffect, useRef } from "react";
export const TableOfContents = ({ staticPosition, noh3s }) => {
const { nestedHeadings } = useHeadingsData(noh3s);
const [activeId, setActiveId] = useState();
useIntersectionObserver(setActiveId);
return (
);
};
const getNestedHeadings = (headingElements) => {
const nestedHeadings = [];
headingElements.forEach((heading, index) => {
const { innerText: title, id } = heading;
if (heading.nodeName === "H2") {
nestedHeadings.push({ id, title, items: [] });
} else if (heading.nodeName === "H3" && nestedHeadings.length > 0) {
nestedHeadings[nestedHeadings.length - 1].items.push({
id,
title,
});
}
});
return nestedHeadings;
};
const useHeadingsData = (noh3s) => {
const [nestedHeadings, setNestedHeadings] = useState([]);
const query = noh3s ? "h2" : "h2, h3";
useEffect(() => {
const headingElements = Array.from(document.querySelectorAll(query));
const newNestedHeadings = getNestedHeadings(headingElements);
setNestedHeadings(newNestedHeadings);
}, []);
return { nestedHeadings };
};
const Headings = ({ headings, activeId }) => (
{headings.map((heading, index) => (
-
{heading.title}
{heading.items.length > 0 && (
)}
))}
);
const useIntersectionObserver = (setActiveId) => {
const headingElementsRef = useRef({});
useEffect(() => {
const callback = (headings) => {
headingElementsRef.current = headings.reduce((map, headingElement) => {
map[headingElement.target.id] = headingElement;
return map;
}, headingElementsRef.current);
const visibleHeadings = [];
Object.keys(headingElementsRef.current).forEach((key) => {
const headingElement = headingElementsRef.current[key];
if (headingElement.isIntersecting) visibleHeadings.push(headingElement);
});
const getIndexFromId = (id) =>
headingElements.findIndex((heading) => heading.id === id);
if (visibleHeadings.length === 1) {
setActiveId(visibleHeadings[0].target.id);
} else if (visibleHeadings.length > 1) {
const sortedVisibleHeadings = visibleHeadings.sort(
(a, b) => getIndexFromId(a.target.id) > getIndexFromId(b.target.id)
);
setActiveId(sortedVisibleHeadings[0].target.id);
}
};
const observer = new IntersectionObserver(callback, {
rootMargin: "0px 0px -50% 0px",
});
const headingElements = Array.from(document.querySelectorAll("h2, h3"));
headingElements.forEach((element) => observer.observe(element));
return () => observer.disconnect();
}, [setActiveId]);
};