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:
parent
c1fc2b759a
commit
dfc6d085e0
2 changed files with 117 additions and 66 deletions
|
@ -36,11 +36,23 @@ const pageTitle: string = `${post.data.title} | ${SITE.title}`;
|
||||||
format={"avif"}
|
format={"avif"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="pin" class="mt-10">
|
<div id="pin" class="mt-10 hidden space-y-4 md:block">
|
||||||
<div class="mb-4 h-0.5 w-full bg-[#fa5a15]" id="progress"></div>
|
<div class="h-px w-full bg-neutral-300 dark:bg-neutral-700">
|
||||||
<h2 class="text-pretty text-sm font-light text-neutral-500">
|
<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:
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -63,33 +75,37 @@ const pageTitle: string = `${post.data.title} | ${SITE.title}`;
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|
||||||
<style is:global>
|
<style is:global>
|
||||||
h2,
|
:root {
|
||||||
h3,
|
--transition-cubic: cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||||
h4,
|
}
|
||||||
h5,
|
|
||||||
h6 {
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
article h2,
|
||||||
|
article h3,
|
||||||
|
article h4,
|
||||||
|
article h5,
|
||||||
|
article h6 {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-top: 2.5rem;
|
margin-top: 2.5rem;
|
||||||
|
scroll-margin-top: 3rem;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes grow-progress {
|
@keyframes grow-progress {
|
||||||
from {
|
from {
|
||||||
transform: scaleX(0);
|
transform: scaleX(0);
|
||||||
|
@ -98,12 +114,32 @@ const pageTitle: string = `${post.data.title} | ${SITE.title}`;
|
||||||
transform: scaleX(1);
|
transform: scaleX(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#progress {
|
#progress {
|
||||||
transform-origin: 0 50%;
|
transform-origin: 0 50%;
|
||||||
animation: grow-progress auto linear;
|
animation: grow-progress auto linear;
|
||||||
animation-timeline: scroll(block root);
|
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>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -117,10 +153,76 @@ const pageTitle: string = `${post.data.title} | ${SITE.title}`;
|
||||||
scrub: 1,
|
scrub: 1,
|
||||||
pin: true,
|
pin: true,
|
||||||
trigger: "#pin",
|
trigger: "#pin",
|
||||||
markers: true,
|
|
||||||
start: "top 20%",
|
start: "top 20%",
|
||||||
endTrigger: "footer",
|
endTrigger: "footer",
|
||||||
end: "top bottom",
|
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>
|
</script>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue