Skip to content

Commit e999f87

Browse files
committed
改进目录滚动突出显示和活动状态
1 parent f22e08b commit e999f87

File tree

2 files changed

+156
-23
lines changed

2 files changed

+156
-23
lines changed

templates/assets/js/pageforge.js

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,20 +417,174 @@ const FontSizeControl = {
417417
}
418418
};
419419

420+
const TOC = {
421+
init() {
422+
// 添加样式
423+
if (!document.getElementById('toc-styles')) {
424+
const style = document.createElement('style');
425+
style.id = 'toc-styles';
426+
style.textContent = `
427+
.toc-link.active-toc-item {
428+
background-color: rgb(239 246 255);
429+
color: rgb(37 99 235);
430+
position: relative;
431+
}
432+
.dark .toc-link.active-toc-item {
433+
background-color: rgba(30 58 138 / 0.2);
434+
color: rgb(96 165 250);
435+
}
436+
.toc-link.active-toc-item::before {
437+
content: '';
438+
position: absolute;
439+
left: 0;
440+
top: 50%;
441+
transform: translateY(-50%);
442+
width: 3px;
443+
height: 16px;
444+
background-color: rgb(37 99 235);
445+
border-top-right-radius: 2px;
446+
border-bottom-right-radius: 2px;
447+
}
448+
.dark .toc-link.active-toc-item::before {
449+
background-color: rgb(96 165 250);
450+
}
451+
`;
452+
document.head.appendChild(style);
453+
}
454+
455+
// 获取所有 TOC 链接
456+
const tocLinks = document.querySelectorAll('.toc-link');
457+
458+
// 处理点击事件
459+
tocLinks.forEach(link => {
460+
link.addEventListener('click', (e) => {
461+
e.preventDefault();
462+
463+
// 移除所有活跃状态
464+
tocLinks.forEach(l => l.classList.remove('active-toc-item'));
465+
466+
// 添加当前项的活跃状态
467+
link.classList.add('active-toc-item');
468+
469+
// 获取目标元素和滚动位置
470+
const targetId = link.getAttribute('data-slug');
471+
const target = document.getElementById(targetId);
472+
const header = document.querySelector('header nav');
473+
const offset = header ? header.offsetHeight + 10 : 80;
474+
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - offset;
475+
476+
// 滚动到目标位置
477+
window.scrollTo({
478+
top: targetPosition,
479+
behavior: 'smooth'
480+
});
481+
482+
// 如果是移动端,关闭 TOC 面板
483+
const tocMobile = document.getElementById('toc-mobile');
484+
if (tocMobile) {
485+
tocMobile.classList.add('translate-y-full');
486+
}
487+
});
488+
});
489+
490+
// 监听滚动,更新当前活跃项
491+
const updateActiveItem = () => {
492+
const header = document.querySelector('header nav');
493+
const headerHeight = header ? header.offsetHeight : 0;
494+
495+
// 查找所有带 slug 的容器
496+
const headings = Array.from(document.querySelectorAll('[id].inline-flex'));
497+
498+
// 找到当前视窗中最靠上的标题
499+
let current = null;
500+
let minDistance = Infinity;
501+
502+
headings.forEach(heading => {
503+
const rect = heading.getBoundingClientRect();
504+
const top = rect.top - headerHeight - 20;
505+
506+
// 计算到视窗顶部的距离
507+
const distance = Math.abs(top);
508+
509+
// 如果元素在视窗上方或接近顶部,且距离比当前最小距离更小
510+
if (top <= 10 && distance < minDistance) {
511+
minDistance = distance;
512+
current = heading;
513+
}
514+
});
515+
516+
// 更新 TOC 活跃状态
517+
if (current) {
518+
const slug = current.id;
519+
const tocLinks = document.querySelectorAll('.toc-link');
520+
521+
tocLinks.forEach(link => {
522+
if (link.getAttribute('data-slug') === slug) {
523+
link.classList.add('active-toc-item');
524+
525+
// 确保活跃项在滚动区域内可见
526+
const nav = link.closest('.overflow-y-auto');
527+
if (nav) {
528+
const navRect = nav.getBoundingClientRect();
529+
const linkRect = link.getBoundingClientRect();
530+
531+
if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) {
532+
link.scrollIntoView({behavior: 'smooth', block: 'center'});
533+
}
534+
}
535+
}
536+
else {
537+
link.classList.remove('active-toc-item');
538+
}
539+
});
540+
}
541+
};
542+
543+
// 使用 IntersectionObserver 来优化滚动监听
544+
const observer = new IntersectionObserver((entries) => {
545+
entries.forEach(() => {
546+
requestAnimationFrame(updateActiveItem);
547+
});
548+
}, {
549+
rootMargin: '-20% 0px -80% 0px',
550+
threshold: [0, 1]
551+
});
552+
553+
// 观察所有标题元素
554+
document.querySelectorAll('[id].inline-flex').forEach(heading => {
555+
observer.observe(heading);
556+
});
557+
558+
// 仍然保留滚动监听作为备份
559+
let scrollTimeout;
560+
window.addEventListener('scroll', () => {
561+
if (scrollTimeout) {
562+
window.cancelAnimationFrame(scrollTimeout);
563+
}
564+
scrollTimeout = window.requestAnimationFrame(updateActiveItem);
565+
}, {passive: true});
566+
567+
// 初始调用一次
568+
updateActiveItem();
569+
}
570+
};
571+
420572
// 暴露到全局
421573
window.PageForge = {
422574
GitHubStats,
423575
CodeCopy,
424576
Header,
425577
FontSizeControl,
426-
Banner
578+
Banner,
579+
TOC
427580
};
428581

429582
// DOM 加载完成后初始化
430583
if (document.readyState === 'loading') {
431584
document.addEventListener('DOMContentLoaded', () => {
432585
Header.init();
433586
FontSizeControl.init();
587+
TOC.init();
434588
});
435589
}
436590
else {

templates/includes/toc.ejs

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@
88
<% items?.forEach(item => { %>
99
<div class="pl-<%= (item.level - 1) * 4 %>">
1010
<a href="#<%= item.slug %>"
11-
onclick="event.preventDefault();
12-
const target = document.getElementById('<%= item.slug %>');
13-
const header = document.querySelector('header nav');
14-
const offset = header ? header.offsetHeight + 10 : 80;
15-
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - offset;
16-
window.scrollTo({top: targetPosition, behavior: 'smooth'});"
1711
class="block px-3 py-1 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md transition-colors duration-150 toc-link"
1812
data-slug="<%= item.slug %>">
1913
<%- item.text %>
@@ -29,15 +23,7 @@
2923
</div>
3024
</div>
3125
32-
<!-- 移动端目录按钮 -->
33-
<button class="lg:hidden fixed right-4 bottom-4 z-20 bg-white dark:bg-gray-800 p-2 rounded-full shadow-lg"
34-
onclick="document.getElementById('toc-mobile').classList.toggle('translate-y-full')">
35-
<svg class="w-6 h-6 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"></path>
37-
</svg>
38-
</button>
39-
40-
<!-- 移动端底部目录面板 -->
26+
<!-- 移动端的模板也需要同样的修改 -->
4127
<div id="toc-mobile"
4228
class="lg:hidden fixed bottom-0 inset-x-0 z-30 bg-white dark:bg-gray-800
4329
transform translate-y-full transition duration-200 ease-in-out
@@ -57,13 +43,6 @@
5743
<% items?.forEach(item => { %>
5844
<div class="pl-<%= (item.level - 1) * 4 %>">
5945
<a href="#<%= item.slug %>"
60-
onclick="event.preventDefault();
61-
const target = document.getElementById('<%= item.slug %>');
62-
const header = document.querySelector('header nav');
63-
const offset = header ? header.offsetHeight + 20 : 80;
64-
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - offset;
65-
window.scrollTo({top: targetPosition, behavior: 'smooth'});
66-
document.getElementById('toc-mobile').classList.add('translate-y-full');"
6746
class="block px-3 py-1 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md transition-colors duration-150 toc-link"
6847
data-slug="<%= item.slug %>">
6948
<%- item.text %>

0 commit comments

Comments
 (0)