Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

优化计算内容距离菜单位置 #35

Merged
merged 2 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pageforge",
"version": "2025.1.3",
"version": "2025.1.4",
"description": "PageForge 是一款现代化的静态页面生成与部署平台,旨在帮助用户快速创建精美的静态网站,并一键部署到 GitHub Pages。 无论是个人博客、项目文档还是企业官网,PageForge 都能让你轻松实现高效构建、智能部署和即时上线。",
"homepage": "https://pageforge.devlive.org",
"repository": {
Expand Down
243 changes: 242 additions & 1 deletion templates/assets/js/pageforge.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,91 @@ const CodeCopy = {
}
};

// Banner 和 Header 高度管理模块
const Banner = {
updateHeaderHeight() {
requestAnimationFrame(() => {
const header = DOMUtils.find('header nav');
if (header) {
const height = header.offsetHeight;
document.documentElement.style.setProperty('--header-height', height + 'px');

// 更新 sidebar 和 toc 的 top 位置
const sidebar = DOMUtils.find('#sidebar-menu nav');
const toc = DOMUtils.find('#toc-container nav');

if (sidebar) {
// 更新 sidebar 的 top 位置
sidebar.style.top = `calc(var(--header-height) + 1rem)`;
}

if (toc) {
// 更新 toc 的 top 位置
toc.style.top = `calc(var(--header-height) + 1rem)`;
}
}
});
},

close(button) {
const banner = button.closest('[data-banner]');
if (!banner) {
return;
}

// 移除之前的监听器
if (window._bannerObserver) {
window._bannerObserver.disconnect();
}

// 添加过渡效果
banner.style.transition = 'height 0.2s ease-out, opacity 0.2s ease-out';
banner.style.height = banner.offsetHeight + 'px';
banner.style.opacity = '1';

// 强制重绘
banner.offsetHeight;

// 开始动画
banner.style.height = '0';
banner.style.opacity = '0';
banner.style.overflow = 'hidden';

// 动画结束后移除元素并更新高度
banner.addEventListener('transitionend', function handler() {
banner.remove();
Banner.updateHeaderHeight();
banner.removeEventListener('transitionend', handler);
});
},

init() {
// 初始化 header 高度
this.updateHeaderHeight();

// 监听窗口大小变化
window.addEventListener('resize', () => this.updateHeaderHeight());

// 监听 banner 的变化
const banner = DOMUtils.find('[data-banner]');
if (banner) {
window._bannerObserver = new MutationObserver(() => this.updateHeaderHeight());
window._bannerObserver.observe(banner, {
attributes: true,
childList: true,
subtree: true,
characterData: true
});

// 绑定关闭按钮事件
const closeButton = DOMUtils.find('[data-banner-close]', banner);
if (closeButton) {
closeButton.addEventListener('click', () => this.close(closeButton));
}
}
}
};

// Header 组件
const Header = {
DarkMode: {
Expand Down Expand Up @@ -280,6 +365,7 @@ const Header = {
init() {
this.DarkMode.init();
this.initEventListeners();
Banner.init();

const observer = new MutationObserver(() => {
this.initEventListeners();
Expand Down Expand Up @@ -331,19 +417,174 @@ const FontSizeControl = {
}
};

const TOC = {
init() {
// 添加样式
if (!document.getElementById('toc-styles')) {
const style = document.createElement('style');
style.id = 'toc-styles';
style.textContent = `
.toc-link.active-toc-item {
background-color: rgb(239 246 255);
color: rgb(37 99 235);
position: relative;
}
.dark .toc-link.active-toc-item {
background-color: rgba(30 58 138 / 0.2);
color: rgb(96 165 250);
}
.toc-link.active-toc-item::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background-color: rgb(37 99 235);
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
.dark .toc-link.active-toc-item::before {
background-color: rgb(96 165 250);
}
`;
document.head.appendChild(style);
}

// 获取所有 TOC 链接
const tocLinks = document.querySelectorAll('.toc-link');

// 处理点击事件
tocLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();

// 移除所有活跃状态
tocLinks.forEach(l => l.classList.remove('active-toc-item'));

// 添加当前项的活跃状态
link.classList.add('active-toc-item');

// 获取目标元素和滚动位置
const targetId = link.getAttribute('data-slug');
const target = document.getElementById(targetId);
const header = document.querySelector('header nav');
const offset = header ? header.offsetHeight + 10 : 80;
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - offset;

// 滚动到目标位置
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});

// 如果是移动端,关闭 TOC 面板
const tocMobile = document.getElementById('toc-mobile');
if (tocMobile) {
tocMobile.classList.add('translate-y-full');
}
});
});

// 监听滚动,更新当前活跃项
const updateActiveItem = () => {
const header = document.querySelector('header nav');
const headerHeight = header ? header.offsetHeight : 0;

// 查找所有带 slug 的容器
const headings = Array.from(document.querySelectorAll('[id].inline-flex'));

// 找到当前视窗中最靠上的标题
let current = null;
let minDistance = Infinity;

headings.forEach(heading => {
const rect = heading.getBoundingClientRect();
const top = rect.top - headerHeight - 20;

// 计算到视窗顶部的距离
const distance = Math.abs(top);

// 如果元素在视窗上方或接近顶部,且距离比当前最小距离更小
if (top <= 10 && distance < minDistance) {
minDistance = distance;
current = heading;
}
});

// 更新 TOC 活跃状态
if (current) {
const slug = current.id;
const tocLinks = document.querySelectorAll('.toc-link');

tocLinks.forEach(link => {
if (link.getAttribute('data-slug') === slug) {
link.classList.add('active-toc-item');

// 确保活跃项在滚动区域内可见
const nav = link.closest('.overflow-y-auto');
if (nav) {
const navRect = nav.getBoundingClientRect();
const linkRect = link.getBoundingClientRect();

if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) {
link.scrollIntoView({behavior: 'smooth', block: 'center'});
}
}
}
else {
link.classList.remove('active-toc-item');
}
});
}
};

// 使用 IntersectionObserver 来优化滚动监听
const observer = new IntersectionObserver((entries) => {
entries.forEach(() => {
requestAnimationFrame(updateActiveItem);
});
}, {
rootMargin: '-20% 0px -80% 0px',
threshold: [0, 1]
});

// 观察所有标题元素
document.querySelectorAll('[id].inline-flex').forEach(heading => {
observer.observe(heading);
});

// 仍然保留滚动监听作为备份
let scrollTimeout;
window.addEventListener('scroll', () => {
if (scrollTimeout) {
window.cancelAnimationFrame(scrollTimeout);
}
scrollTimeout = window.requestAnimationFrame(updateActiveItem);
}, {passive: true});

// 初始调用一次
updateActiveItem();
}
};

// 暴露到全局
window.PageForge = {
GitHubStats,
CodeCopy,
Header,
FontSizeControl
FontSizeControl,
Banner,
TOC
};

// DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
Header.init();
FontSizeControl.init();
TOC.init();
});
}
else {
Expand Down
2 changes: 1 addition & 1 deletion templates/includes/header-banner.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<%- locals.siteData.banner.content %>
</div>
<button class="text-blue-500 hover:text-blue-700 <%= darkClasses('dark:text-blue-400 dark:hover:text-blue-200') %>"
onclick="this.closest('[data-banner]').remove()">
data-banner-close>
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"/>
</svg>
Expand Down
25 changes: 6 additions & 19 deletions templates/includes/toc.ejs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<div class="hidden lg:block flex-none w-64 pl-8 mr-8">
<div class="sticky top-20">
<div class="sticky" style="top: calc(var(--header-height) + 1rem)">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-4">目录</div>
<nav class="overflow-y-auto max-h-[calc(100vh-8rem)] pr-4 -mr-4 space-y-1 pageforge-scrollbar"
<nav class="overflow-y-auto pr-4 -mr-4 space-y-1 pageforge-scrollbar"
style="max-height: calc(100vh - var(--header-height) - 8rem);"
id="table-of-contents">
<% function renderTocItem(items) { %>
<% items?.forEach(item => { %>
<div class="pl-<%= (item.level - 1) * 4 %>">
<a href="#<%= item.slug %>"
onclick="event.preventDefault(); const target = document.getElementById('<%= item.slug %>'); const offset = 80; const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - offset; window.scrollTo({top: targetPosition, behavior: 'smooth'});"
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"
data-slug="<%= item.slug %>">
<%- item.text %>
Expand All @@ -23,15 +23,7 @@
</div>
</div>

<!-- 移动端目录按钮 -->
<button class="lg:hidden fixed right-4 bottom-4 z-20 bg-white dark:bg-gray-800 p-2 rounded-full shadow-lg"
onclick="document.getElementById('toc-mobile').classList.toggle('translate-y-full')">
<svg class="w-6 h-6 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"></path>
</svg>
</button>

<!-- 移动端底部目录面板 -->
<!-- 移动端的模板也需要同样的修改 -->
<div id="toc-mobile"
class="lg:hidden fixed bottom-0 inset-x-0 z-30 bg-white dark:bg-gray-800
transform translate-y-full transition duration-200 ease-in-out
Expand All @@ -45,17 +37,12 @@
</svg>
</button>
</div>
<nav class="overflow-y-auto max-h-[60vh] space-y-1">
<nav class="overflow-y-auto space-y-1"
style="max-height: calc(60vh - var(--header-height));">
<% function renderTocItemMobile(items) { %>
<% items?.forEach(item => { %>
<div class="pl-<%= (item.level - 1) * 4 %>">
<a href="#<%= item.slug %>"
onclick="event.preventDefault();
const target = document.getElementById('<%= item.slug %>');
const offset = 80;
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({top: targetPosition, behavior: 'smooth'});
document.getElementById('toc-mobile').classList.add('translate-y-full');"
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"
data-slug="<%= item.slug %>">
<%- item.text %>
Expand Down
Loading