Skip to content

Commit

Permalink
Merge pull request #35 from devlive-community/dev
Browse files Browse the repository at this point in the history
优化计算内容距离菜单位置
  • Loading branch information
qianmoQ authored Feb 6, 2025
2 parents 424336e + e999f87 commit 93e1286
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 53 deletions.
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

0 comments on commit 93e1286

Please sign in to comment.