120 lines
3.3 KiB
Plaintext
120 lines
3.3 KiB
Plaintext
---
|
|
import type { MarkdownHeading } from "astro";
|
|
import "../styles/toc.scss";
|
|
|
|
interface Props {
|
|
toc: boolean;
|
|
headings: MarkdownHeading[];
|
|
}
|
|
|
|
const { toc, headings } = Astro.props;
|
|
const minDepth = Math.min(...headings.map((heading) => heading.depth));
|
|
---
|
|
|
|
{
|
|
toc ? (
|
|
<>
|
|
<div class="toc-wrapper">
|
|
<slot />
|
|
<div class="toc">
|
|
<h3 class="title">Table of contents</h3>
|
|
<ul>
|
|
{headings.map((heading) => {
|
|
return (
|
|
<li>
|
|
<a href={`#${heading.slug}`} id={`${heading.slug}-link`}>
|
|
<span style={`padding-left: ${(heading.depth - minDepth) * 10}px;`}>
|
|
{heading.text}
|
|
</span>
|
|
</a>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<slot />
|
|
)
|
|
}
|
|
|
|
<script define:vars={{ toc, headings }}>
|
|
if (toc) {
|
|
const headingTags = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
|
|
const headingsMap = new Map([...headings.map((heading) => [heading.slug, new Set()])]);
|
|
const reverseHeadingMap = new Map();
|
|
const linkMap = new Map();
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const visibleElements = new Map();
|
|
|
|
// Register links
|
|
for (const heading of headings) {
|
|
const link = document.getElementById(`${heading.slug}-link`);
|
|
const el = document.getElementById(heading.slug);
|
|
if (link && el) {
|
|
linkMap.set(heading.slug, link);
|
|
link.addEventListener("click", (e) => {
|
|
el.scrollIntoView({ behavior: "smooth" });
|
|
e.preventDefault();
|
|
});
|
|
}
|
|
if (!visibleElements.has(heading.slug)) {
|
|
visibleElements.set(heading.slug, new Set());
|
|
}
|
|
}
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
for (const entry of entries) {
|
|
const target = entry.target;
|
|
const slug = reverseHeadingMap.get(target);
|
|
const link = linkMap.get(slug);
|
|
const associatedEls = visibleElements.get(slug);
|
|
|
|
if (entry.isIntersecting) {
|
|
// if it wasn't previously visible
|
|
// let's make the link active
|
|
if (associatedEls.size === 0) {
|
|
link.parentNode.classList.add("active");
|
|
}
|
|
|
|
associatedEls.add(target);
|
|
} else {
|
|
// if it was previously visible
|
|
// check if it's the last element
|
|
if (associatedEls.size > 0) {
|
|
if (associatedEls.size === 1) link.parentNode.classList.remove("active");
|
|
}
|
|
|
|
if (associatedEls.size > 0) {
|
|
associatedEls.delete(target);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const postContentEl = document.getElementById("post-content");
|
|
|
|
let belongsTo;
|
|
for (const child of postContentEl.children) {
|
|
if (headingTags.has(child.tagName.toLowerCase())) {
|
|
belongsTo = child.id;
|
|
}
|
|
|
|
if (belongsTo) {
|
|
const headingSet = headingsMap.get(belongsTo);
|
|
headingSet.add(child);
|
|
reverseHeadingMap.set(child, belongsTo);
|
|
}
|
|
}
|
|
|
|
[...headingsMap.values()]
|
|
.flatMap((x) => [...x])
|
|
.forEach((x) => {
|
|
observer.observe(x);
|
|
});
|
|
});
|
|
}
|
|
</script>
|