on master help sign in
docs: try sidebar
commitda7c2af5788e15a881846633a2b5a1d9afb7ce34 authorMatilde Park <matilde@tlon.io> dateThu 22 Jul 2021, 16:57 (5 years ago) parent6d7ebac
1 file changed+112−1
pages/docs/[[...slug]].js+112−1
pages/docs/[[...slug]].js+112−1
@@ -1,7 +1,7 @@
11import Head from "next/head";
22import Link from "next/link";
33import { useRouter } from "next/router";
4−import { useState } from "react";
4+import { useState, useEffect, useRef } from "react";
55import classnames from "classnames";
66import { join } from "path";
77import {
@@ -17,6 +17,116 @@ import Sidebar from "../../components/Sidebar";
1717import markdownStyles from "../../styles/markdown.module.css";
1818import { decode } from "html-entities";
1919
20+const TableOfContents = () => {
21+ const { nestedHeadings } = useHeadingsData();
22+ const [activeId, setActiveId] = useState();
23+ useIntersectionObserver(setActiveId);
24+ return (
25+ <nav
26+ className="sticky min-h-0 overflow-y-scroll w-52 flex-shrink-0 pb-24 hidden lg:block"
27+ style={{ top: "8rem", height: "calc(100vh - 16rem)" }}
28+ >
29+ <Headings headings={nestedHeadings} activeId={activeId} />
30+ </nav>
31+ );
32+};
33+
34+const getNestedHeadings = (headingElements) => {
35+ const nestedHeadings = [];
36+
37+ headingElements.forEach((heading, index) => {
38+ const { innerText: title, id } = heading;
39+
40+ if (heading.nodeName === "H2") {
41+ nestedHeadings.push({ id, title, items: [] });
42+ } else if (heading.nodeName === "H3" && nestedHeadings.length > 0) {
43+ nestedHeadings[nestedHeadings.length - 1].items.push({
44+ id,
45+ title,
46+ });
47+ }
48+ });
49+
50+ return nestedHeadings;
51+};
52+
53+const useHeadingsData = () => {
54+ const [nestedHeadings, setNestedHeadings] = useState([]);
55+
56+ useEffect(() => {
57+ const headingElements = Array.from(document.querySelectorAll("h2, h3"));
58+
59+ const newNestedHeadings = getNestedHeadings(headingElements);
60+ setNestedHeadings(newNestedHeadings);
61+ }, []);
62+
63+ return { nestedHeadings };
64+};
65+
66+const Headings = ({ headings, activeId }) => (
67+ <ul>
68+ {headings.map((heading) => (
69+ <li key={heading.id}>
70+ <a className="font-semibold" href={`#${heading.id}`}>
71+ {heading.title}
72+ </a>
73+ {heading.items.length > 0 && (
74+ <ul className="pl-2">
75+ {heading.items.map((child) => (
76+ <li
77+ key={child.id}
78+ className={child.id === activeId ? "text-green" : "text-gray"}
79+ >
80+ <a href={`#${child.id}`}>{child.title}</a>
81+ </li>
82+ ))}
83+ </ul>
84+ )}
85+ </li>
86+ ))}
87+ </ul>
88+);
89+
90+const useIntersectionObserver = (setActiveId) => {
91+ const headingElementsRef = useRef({});
92+ useEffect(() => {
93+ const callback = (headings) => {
94+ headingElementsRef.current = headings.reduce((map, headingElement) => {
95+ map[headingElement.target.id] = headingElement;
96+ return map;
97+ }, headingElementsRef.current);
98+
99+ const visibleHeadings = [];
100+ Object.keys(headingElementsRef.current).forEach((key) => {
101+ const headingElement = headingElementsRef.current[key];
102+ if (headingElement.isIntersecting) visibleHeadings.push(headingElement);
103+ });
104+
105+ const getIndexFromId = (id) =>
106+ headingElements.findIndex((heading) => heading.id === id);
107+
108+ if (visibleHeadings.length === 1) {
109+ setActiveId(visibleHeadings[0].target.id);
110+ } else if (visibleHeadings.length > 1) {
111+ const sortedVisibleHeadings = visibleHeadings.sort(
112+ (a, b) => getIndexFromId(a.target.id) > getIndexFromId(b.target.id)
113+ );
114+ setActiveId(sortedVisibleHeadings[0].target.id);
115+ }
116+ };
117+
118+ const observer = new IntersectionObserver(callback, {
119+ rootMargin: "0px 0px -50% 0px",
120+ });
121+
122+ const headingElements = Array.from(document.querySelectorAll("h2, h3"));
123+
124+ headingElements.forEach((element) => observer.observe(element));
125+
126+ return () => observer.disconnect();
127+ }, [setActiveId]);
128+};
129+
20130const breadcrumbs = (posts, paths) => {
21131 const results = [
22132 <Link href="/">Urbit</Link>,
@@ -172,6 +282,7 @@ export default function DocsLayout({
172282 )}
173283 </div>
174284 </ContentArea>
285+ <TableOfContents />
175286 </div>
176287 </>
177288 );