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