61 lines
1.6 KiB
JavaScript
61 lines
1.6 KiB
JavaScript
import { useState, useEffect } from "react";
|
|
|
|
/* Creates an Intersection Observer to keep track of headings intersecting
|
|
* a section of the viewport defined by the rootMargin */
|
|
const getIntersectionObserver = (callback) => {
|
|
return new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
callback(entry);
|
|
});
|
|
},
|
|
{
|
|
root: null,
|
|
threshold: 0.55,
|
|
rootMargin: "-65px 0% -85% 0%", // 65px is a navbar height
|
|
}
|
|
);
|
|
};
|
|
|
|
const useHeadingsObserver = () => {
|
|
const [activeHeading, setActiveHeading] = useState("");
|
|
const [observer, setObserver] = useState(null);
|
|
|
|
/* Runs only after the first render, in order to preserve the observer
|
|
* between component rerenderings (e.g. after activeHeading change). */
|
|
useEffect(() => {
|
|
const observer = getIntersectionObserver((entry) => {
|
|
if (entry.isIntersecting) {
|
|
setActiveHeading(entry.target.id);
|
|
}
|
|
});
|
|
setObserver(observer);
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
}, []);
|
|
|
|
/* On initial render activeHeading will be `null`, since the observer
|
|
* has not been instantiated yet. However, we still want to highlight
|
|
* the current heading in the ToC based on the current url. */
|
|
useEffect(() => {
|
|
if (!activeHeading) {
|
|
return;
|
|
}
|
|
|
|
const tocLink = document.querySelector(
|
|
`.toc-link[href="#${activeHeading}"]`
|
|
);
|
|
tocLink.classList.add("active");
|
|
|
|
return () => {
|
|
tocLink.classList.remove("active");
|
|
};
|
|
}, [activeHeading]);
|
|
|
|
return observer;
|
|
};
|
|
|
|
export default useHeadingsObserver;
|