Remove generateToc file and refactor TOC in insights page

The generateToc.ts file has been deleted and the way Table of Contents is generated in the insights page has been refactored. The refactoring changes include graphical enhancements, making the UI more dynamic, and implementing GSAP's ScrollTrigger for the headings. Increased readability and easier code maintenance are also anticipated from this.
This commit is contained in:
Emil Gulamov 2024-04-02 20:21:57 +04:00
parent c1fc2b759a
commit dfc6d085e0
2 changed files with 117 additions and 66 deletions

View file

@ -36,11 +36,23 @@ const pageTitle: string = `${post.data.title} | ${SITE.title}`;
format={"avif"}
/>
</div>
<div id="pin" class="mt-10">
<div class="mb-4 h-0.5 w-full bg-[#fa5a15]" id="progress"></div>
<h2 class="text-pretty text-sm font-light text-neutral-500">
<div id="pin" class="mt-10 hidden space-y-4 md:block">
<div class="h-px w-full bg-neutral-300 dark:bg-neutral-700">
<div
id="progress"
class="h-px w-full bg-gradient-to-r from-[#fa5a15]/30 to-[#fa5a15]"
>
</div>
</div>
<p class="text-pretty text-sm font-light text-neutral-500">
Table of Contents:
</h2>
</p>
<div id="toc" class="">
<ul
class="space-y-2 text-pretty text-base text-neutral-700 transition duration-300 dark:text-neutral-400"
>
</ul>
</div>
</div>
</div>
@ -63,33 +75,37 @@ const pageTitle: string = `${post.data.title} | ${SITE.title}`;
</MainLayout>
<style is:global>
h2,
h3,
h4,
h5,
h6 {
:root {
--transition-cubic: cubic-bezier(0.165, 0.84, 0.44, 1);
}
html {
scroll-behavior: smooth;
}
article h2,
article h3,
article h4,
article h5,
article h6 {
font-weight: bold;
margin-top: 2.5rem;
scroll-margin-top: 3rem;
}
h2 {
font-size: 1.5rem;
line-height: 2rem;
}
h3 {
font-size: 1.25rem;
line-height: 1.75rem;
}
h4 {
font-size: 1.125rem;
line-height: 1.75rem;
}
p {
margin-top: 1.5rem;
}
@keyframes grow-progress {
from {
transform: scaleX(0);
@ -98,12 +114,32 @@ const pageTitle: string = `${post.data.title} | ${SITE.title}`;
transform: scaleX(1);
}
}
#progress {
transform-origin: 0 50%;
animation: grow-progress auto linear;
animation-timeline: scroll(block root);
}
#toc li {
display: flex;
align-items: center;
opacity: 0.8;
transition: all 300ms var(--transition-cubic);
}
#toc li.selected {
opacity: 1;
}
#toc li svg {
width: 0;
height: 0;
transition:
height 400ms var(--transition-cubic),
width 400ms var(--transition-cubic);
}
#toc li.selected svg {
width: 1.25rem;
height: 1.25rem;
margin-right: 0.3rem;
}
</style>
<script>
@ -117,10 +153,76 @@ const pageTitle: string = `${post.data.title} | ${SITE.title}`;
scrub: 1,
pin: true,
trigger: "#pin",
markers: true,
start: "top 20%",
endTrigger: "footer",
end: "top bottom",
},
});
document.addEventListener("DOMContentLoaded", function () {
// The article element that contains the Markdown content
const article: HTMLElement | null = document.querySelector("article");
// The ToC container <ul> element
const tocList: HTMLElement | null = document.querySelector("#toc ul");
// Function to create and add an item to the ToC list
function addToC(heading: HTMLElement) {
const li = document.createElement("li");
li.className = "toc-level-" + heading.tagName.toLowerCase();
const tempDiv = document.createElement("div");
tempDiv.innerHTML =
'<svg class="w-0 h-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#fa5a15"><path stroke-linecap="round" stroke-linejoin="round" d="m12.75 15 3-3m0 0-3-3m3 3h-7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"></svg>';
const svg = tempDiv.firstChild;
li.appendChild(svg as Node);
const link = document.createElement("a");
link.href = "#" + heading.id;
link.textContent = heading.textContent;
li.appendChild(link);
tocList?.appendChild(li);
return li;
}
// Helper function to toggle the 'selected' class
function setActiveLink(id: string) {
document.querySelectorAll("#toc li").forEach((li) => {
li.classList.remove("selected");
});
const activeLink = document.querySelector(`#toc a[href="#${id}"]`);
if (activeLink) {
const li: HTMLElement | null = activeLink.parentElement;
li?.classList.add("selected");
}
}
// Observe headings and add them to the ToC
let headings: NodeListOf<HTMLElement> | [] = article
? article.querySelectorAll("h1, h2, h3, h4, h5, h6")
: [];
headings.forEach((heading: Element, i: number) => {
if (heading instanceof HTMLElement) {
addToC(heading);
gsap.timeline({
scrollTrigger: {
trigger: heading,
start: "top 20%",
end: () =>
`bottom top+=${i === headings.length - 1 ? 0 : headings[i + 1].getBoundingClientRect().height}`,
onEnter: () => setActiveLink(heading.id),
onLeaveBack: () =>
setActiveLink((headings[i - 1] || { id: null }).id),
},
});
}
});
});
</script>

View file

@ -1,51 +0,0 @@
// https://github.com/withastro/docs/blob/882e0b0a9d16d1c822cb8c230a62a4bfcd308605/src/util/generateToc.ts
import type { MarkdownHeading } from 'astro';
export interface TocItem extends MarkdownHeading {
children: TocItem[];
}
function diveChildren(item: TocItem, depth: number): TocItem[] {
if (depth === 1) {
return item.children;
} else {
// e.g., 2
return diveChildren(item.children[item.children.length - 1], depth - 1);
}
}
export default function generateToc(headings: MarkdownHeading[], title = 'Overview') {
const overview = { depth: 2, slug: 'overview', text: title };
headings = [overview, ...headings.filter(({ depth }) => depth > 1 && depth < 4)];
const toc: Array<TocItem> = [];
for (const heading of headings) {
if (toc.length === 0) {
toc.push({
...heading,
children: [],
});
} else {
const lastItemInToc = toc[toc.length - 1];
if (heading.depth < lastItemInToc.depth) {
throw new Error(`Orphan heading found: ${heading.text}.`);
}
if (heading.depth === lastItemInToc.depth) {
// same depth
toc.push({
...heading,
children: [],
});
} else {
// higher depth
// push into children, or children' children alike
const gap = heading.depth - lastItemInToc.depth;
const target = diveChildren(lastItemInToc, gap);
target.push({
...heading,
children: [],
});
}
}
}
return toc;
}