Skip to content

Commit 67ecf2c

Browse files
committed
Update toc with active section.
1 parent 2e35435 commit 67ecf2c

File tree

2 files changed

+94
-9
lines changed

2 files changed

+94
-9
lines changed

src/content/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const pages = defineCollection({
1010
schema: z.object({
1111
title: z.string(),
1212
subtitle: z.string(),
13-
toc: z.boolean().optional().default(false),
13+
toc: z.boolean().optional().default(true),
1414
}),
1515
});
1616

src/pages/[...slug].astro

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,8 @@ const description = post.data.subtitle;
5454

5555
{ post.data.toc ?
5656
<TwoCols>
57-
<div class="mt-28" > aaa </div>
5857
<Fragment slot="content">
59-
<Headline id="contents" as="h4" title="Contents" />
58+
<Headline id="contents" as="h4" title="Table of contents" />
6059
<div id="toc" class="mb-6 last:mb-0 rounded-lg shadow-md bg-white p-6"></div>
6160
</Fragment>
6261
<Fragment slot="sidebar">
@@ -107,26 +106,112 @@ document.addEventListener("DOMContentLoaded", () => {
107106
const tocContainer = document.getElementById("toc");
108107
if (!tocContainer) return;
109108

110-
const headings = document.querySelectorAll("article h2, article h3");
109+
const headings = Array.from(document.querySelectorAll("article h2, article h3, article h4"));
111110
if (!headings.length) return;
112111

113-
const ul = document.createElement("ul");
112+
const rootUl = document.createElement("ul");
113+
let currentUl = rootUl;
114+
let lastLevel = 2;
115+
const parents = [rootUl];
114116

115117
headings.forEach((heading, index) => {
116118
if (!heading.id) {
117119
heading.id = `heading-${index}`;
118120
}
119121

122+
const level = parseInt(heading.tagName[1], 10);
120123
const li = document.createElement("li");
121-
li.style.marginLeft = heading.tagName === "H3" ? "1em" : "0";
122124
const a = document.createElement("a");
123125
a.href = `#${heading.id}`;
124126
a.textContent = heading.textContent;
125-
126127
li.appendChild(a);
127-
ul.appendChild(li);
128+
129+
if (level > lastLevel) {
130+
const newUl = document.createElement("ul");
131+
parents[parents.length - 1].lastElementChild?.appendChild(newUl);
132+
parents.push(newUl);
133+
} else if (level < lastLevel) {
134+
parents.splice(-(lastLevel - level));
135+
}
136+
137+
currentUl = parents[parents.length - 1];
138+
currentUl.appendChild(li);
139+
lastLevel = level;
128140
});
129141

130-
tocContainer.appendChild(ul);
142+
tocContainer.appendChild(rootUl);
143+
144+
// Scroll spy: highlight active link
145+
const observer = new IntersectionObserver(
146+
entries => {
147+
entries.forEach(entry => {
148+
const id = entry.target.id;
149+
const tocLink = tocContainer.querySelector(`a[href="#${id}"]`);
150+
if (tocLink) {
151+
if (entry.isIntersecting) {
152+
tocContainer.querySelectorAll("a").forEach(a => a.classList.remove("active"));
153+
tocLink.classList.add("active");
154+
}
155+
}
156+
});
157+
},
158+
{
159+
rootMargin: "-30% 0px -60% 0px", // adjust when to highlight
160+
threshold: 0,
161+
}
162+
);
163+
164+
headings.forEach(heading => observer.observe(heading));
131165
});
132166
</script>
167+
168+
<style is:global>
169+
170+
#toc {
171+
font-size: 0.95rem;
172+
line-height: 1.5;
173+
max-width: 300px;
174+
padding: 1rem;
175+
border-left: 1px solid #e0e0e0;
176+
position: sticky;
177+
top: 1rem;
178+
}
179+
180+
#toc ul {
181+
list-style: none;
182+
padding-left: 0;
183+
margin: 0;
184+
}
185+
186+
#toc li {
187+
margin: 0.25em 0;
188+
}
189+
190+
#toc li ul {
191+
margin-left: 1em;
192+
border-left: 1px dashed #ddd;
193+
padding-left: 0.75em;
194+
}
195+
196+
#toc a {
197+
text-decoration: none;
198+
color: #333;
199+
display: block;
200+
padding: 0.25em 0.5em;
201+
border-radius: 4px;
202+
transition: background 0.2s ease, color 0.2s ease;
203+
}
204+
205+
#toc a:hover {
206+
background: #f0f0f0;
207+
color: #007acc;
208+
}
209+
210+
#toc a.active {
211+
font-weight: 600;
212+
color: #007acc;
213+
border-left: 3px solid #007acc;
214+
background: rgba(0, 122, 204, 0.1);
215+
padding-left: 0.5em;
216+
}
217+
</style>

0 commit comments

Comments
 (0)