Skip to content

Commit

Permalink
fix: fixes for the TOC component
Browse files Browse the repository at this point in the history
  • Loading branch information
bzavhorodskyi committed Sep 2, 2023
1 parent 5e056aa commit 14f06d3
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 76 deletions.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"nanostores": "^0.9.3",
"node-html-parser": "^6.1.6",
"parse-numeric-range": "^1.3.0",
"playwright": "^1.37.1",
"preact": "^10.17.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand All @@ -49,10 +50,10 @@
"unified": "~10.1.2",
"unist-util-remove": "^4.0.0",
"unist-util-visit": "^5.0.0",
"unist-util-visit-children": "^3.0.0",
"playwright": "^1.37.1"
"unist-util-visit-children": "^3.0.0"
},
"devDependencies": {
"@types/html-escaper": "^3.0.0",
"@types/mdast": "^4.0.0",
"@types/node": "^20.5.8",
"@types/react": "^18.2.21",
Expand Down
153 changes: 79 additions & 74 deletions src/components/RightSidebar/TableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -1,93 +1,98 @@
import type { MarkdownHeading } from 'astro';
import type { FunctionalComponent } from 'preact';
import { unescape } from 'html-escaper';
import { useState, useEffect, useRef } from 'preact/hooks';
import type { MarkdownHeading } from "astro";
import type { FunctionalComponent } from "preact";
import { unescape } from "html-escaper";
import { useState, useEffect, useRef } from "preact/hooks";

type ItemOffsets = {
id: string;
topOffset: number;
id: string;
topOffset: number;
};

const TableOfContents: FunctionalComponent<{ headings: MarkdownHeading[] }> = ({
headings = [],
headings = [],
}) => {
const toc = useRef<HTMLUListElement>();
const onThisPageID = 'on-this-page-heading';
const itemOffsets = useRef<ItemOffsets[]>([]);
const [currentID, setCurrentID] = useState('overview');
useEffect(() => {
const getItemOffsets = () => {
const titles = document.querySelectorAll('article :is(h1, h2, h3, h4)');
itemOffsets.current = Array.from(titles).map((title) => ({
id: title.id,
topOffset: title.getBoundingClientRect().top + window.scrollY,
}));
};
const toc = useRef<HTMLUListElement>(null);
const onThisPageID = "on-this-page-heading";
const itemOffsets = useRef<ItemOffsets[]>([]);
const [currentID, setCurrentID] = useState("overview");
useEffect(() => {
const getItemOffsets = () => {
const titles = document.querySelectorAll("article :is(h1, h2, h3, h4)");
itemOffsets.current = Array.from(titles).map((title) => ({
id: title.id,
topOffset: title.getBoundingClientRect().top + window.scrollY,
}));
};

getItemOffsets();
window.addEventListener('resize', getItemOffsets);
getItemOffsets();
window.addEventListener("resize", getItemOffsets);

return () => {
window.removeEventListener('resize', getItemOffsets);
};
}, []);
return () => {
window.removeEventListener("resize", getItemOffsets);
};
}, []);

useEffect(() => {
if (!toc.current) return;
useEffect(() => {
if (!toc.current) return;

const setCurrent: IntersectionObserverCallback = (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const { id } = entry.target;
if (id === onThisPageID) continue;
setCurrentID(entry.target.id);
break;
}
}
};
const setCurrent: IntersectionObserverCallback = (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const { id } = entry.target;
if (id === onThisPageID) continue;
setCurrentID(entry.target.id);
break;
}
}
};

const observerOptions: IntersectionObserverInit = {
// Negative top margin accounts for `scroll-margin`.
// Negative bottom margin means heading needs to be towards top of viewport to trigger intersection.
rootMargin: '-100px 0% -66%',
threshold: 1,
};
const observerOptions: IntersectionObserverInit = {
// Negative top margin accounts for `scroll-margin`.
// Negative bottom margin means heading needs to be towards top of viewport to trigger intersection.
rootMargin: "-100px 0% -66%",
threshold: 1,
};

const headingsObserver = new IntersectionObserver(setCurrent, observerOptions);
const headingsObserver = new IntersectionObserver(
setCurrent,
observerOptions
);

// Observe all the headings in the main page content.
document.querySelectorAll('article :is(h1,h2,h3)').forEach((h) => headingsObserver.observe(h));
// Observe all the headings in the main page content.
document
.querySelectorAll("article :is(h1,h2,h3)")
.forEach((h) => headingsObserver.observe(h));

// Stop observing when the component is unmounted.
return () => headingsObserver.disconnect();
}, [toc.current]);
// Stop observing when the component is unmounted.
return () => headingsObserver.disconnect();
}, [toc.current]);

const onLinkClick = (e) => {
setCurrentID(e.target.getAttribute('href').replace('#', ''));
};
const onLinkClick = (e) => {
setCurrentID(e.target.getAttribute("href").replace("#", ""));
};

return (
<>
<h2 id={onThisPageID} className="heading">
On this page
</h2>
<ul ref={toc}>
{headings
.filter(({ depth }) => depth > 1 && depth < 4)
.map((heading) => (
<li
className={`header-link depth-${heading.depth} ${
currentID === heading.slug ? 'current-header-link' : ''
}`.trim()}
>
<a href={`#${heading.slug}`} onClick={onLinkClick}>
{unescape(heading.text)}
</a>
</li>
))}
</ul>
</>
);
return (
<>
<h2 id={onThisPageID} className="heading">
On this page
</h2>
<ul ref={toc}>
{headings
.filter(({ depth }) => depth > 1 && depth < 4)
.map((heading) => (
<li
className={`header-link depth-${heading.depth} ${
currentID === heading.slug ? "current-header-link" : ""
}`.trim()}
>
<a href={`#${heading.slug}`} onClick={onLinkClick}>
{unescape(heading.text)}
</a>
</li>
))}
</ul>
</>
);
};

export default TableOfContents;

0 comments on commit 14f06d3

Please sign in to comment.