1import { useState, useEffect, useRef } from "react";
2import classNames from "classnames";
4export const TableOfContents = ({ staticPosition, noh3s }) => {
5 const { nestedHeadings } = useHeadingsData(noh3s);
6 const [activeId, setActiveId] = useState();
7 useIntersectionObserver(setActiveId);
11 (staticPosition ? "static" : "sticky") +
12 " min-h-0 overflow-y-auto w-52 shrink-0 pb-24 hidden lg:block pl-4"
14 style={{ top: "8rem", height: "calc(100vh - 16rem)" }}
16 <div className="relative after:fixed after:bg-gradient-to-t after:from-white after:via-white after:bottom-0 after:w-52 after:h-60 after:pointer-events-none">
17 <Headings headings={nestedHeadings} activeId={activeId} />
23const getNestedHeadings = (headingElements) => {
24 const nestedHeadings = [];
26 headingElements.forEach((heading, index) => {
27 const { innerText: title, id } = heading;
29 if (heading.nodeName === "H2") {
30 nestedHeadings.push({ id, title, items: [] });
31 } else if (heading.nodeName === "H3" && nestedHeadings.length > 0) {
32 nestedHeadings[nestedHeadings.length - 1].items.push({
39 return nestedHeadings;
42const useHeadingsData = (noh3s) => {
43 const [nestedHeadings, setNestedHeadings] = useState([]);
45 const query = noh3s ? "h2" : "h2, h3";
47 const headingElements = Array.from(document.querySelectorAll(query));
49 const newNestedHeadings = getNestedHeadings(headingElements);
50 setNestedHeadings(newNestedHeadings);
53 return { nestedHeadings };
56const Headings = ({ headings, activeId }) => (
58 className={classNames({
59 hidden: headings.length === 1 && headings?.[0]?.items?.length === 0,
62 {headings.map((heading, index) => (
65 className={classNames({
66 "font-bold": index === 0,
67 "font-medium text-sm": index !== 0,
68 "text-green-400": heading.id === activeId && index !== 0,
70 href={`#${heading.id}`}
74 {heading.items.length > 0 && (
75 <ul className="pl-2 text-xs">
76 {heading.items.map((child) => (
80 child.id === activeId ? "text-green-400" : "text-wall-500"
83 <a href={`#${child.id}`}>{child.title}</a>
93const useIntersectionObserver = (setActiveId) => {
94 const headingElementsRef = useRef({});
96 const callback = (headings) => {
97 headingElementsRef.current = headings.reduce((map, headingElement) => {
98 map[headingElement.target.id] = headingElement;
100 }, headingElementsRef.current);
102 const visibleHeadings = [];
103 Object.keys(headingElementsRef.current).forEach((key) => {
104 const headingElement = headingElementsRef.current[key];
105 if (headingElement.isIntersecting) visibleHeadings.push(headingElement);
108 const getIndexFromId = (id) =>
109 headingElements.findIndex((heading) => heading.id === id);
111 if (visibleHeadings.length === 1) {
112 setActiveId(visibleHeadings[0].target.id);
113 } else if (visibleHeadings.length > 1) {
114 const sortedVisibleHeadings = visibleHeadings.sort(
115 (a, b) => getIndexFromId(a.target.id) > getIndexFromId(b.target.id)
117 setActiveId(sortedVisibleHeadings[0].target.id);
121 const observer = new IntersectionObserver(callback, {
122 rootMargin: "0px 0px -50% 0px",
125 const headingElements = Array.from(document.querySelectorAll("h2, h3"));
127 headingElements.forEach((element) => observer.observe(element));
129 return () => observer.disconnect();
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list