/** * TechBlog Pro - Main JavaScript * * @package TechBlog Pro * @version 1.0.0 */ (function() { 'use strict'; // ==================== Theme Toggle ==================== const ThemeManager = { init() { this.toggle = document.getElementById('theme-toggle'); this.sunIcon = this.toggle?.querySelector('.sun-icon'); this.moonIcon = this.toggle?.querySelector('.moon-icon'); if (!this.toggle) return; // Load saved theme const savedTheme = localStorage.getItem('theme') || 'light'; this.setTheme(savedTheme); // Toggle event this.toggle.addEventListener('click', () => { const current = document.documentElement.getAttribute('data-theme'); this.setTheme(current === 'dark' ? 'light' : 'dark'); }); }, setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); if (this.sunIcon && this.moonIcon) { this.sunIcon.style.display = theme === 'dark' ? 'none' : 'block'; this.moonIcon.style.display = theme === 'dark' ? 'block' : 'none'; } } }; // ==================== Search Overlay ==================== const SearchManager = { init() { this.overlay = document.getElementById('search-overlay'); this.toggle = document.getElementById('search-toggle'); this.input = this.overlay?.querySelector('.search-input'); if (!this.overlay || !this.toggle) return; // Open search this.toggle.addEventListener('click', () => this.open()); // Close on overlay click this.overlay.addEventListener('click', (e) => { if (e.target === this.overlay) this.close(); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { // CMD/CTRL + K to open if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); this.open(); } // ESC to close if (e.key === 'Escape') { this.close(); } }); }, open() { this.overlay.classList.add('active'); setTimeout(() => this.input?.focus(), 100); document.body.style.overflow = 'hidden'; }, close() { this.overlay.classList.remove('active'); document.body.style.overflow = ''; } }; // ==================== Mobile Menu ==================== const MobileMenu = { init() { this.toggle = document.getElementById('mobile-menu-toggle'); this.nav = document.getElementById('main-nav'); if (!this.toggle || !this.nav) return; this.toggle.addEventListener('click', () => { this.nav.classList.toggle('active'); this.toggle.classList.toggle('active'); }); // Close on outside click document.addEventListener('click', (e) => { if (!this.nav.contains(e.target) && !this.toggle.contains(e.target)) { this.nav.classList.remove('active'); this.toggle.classList.remove('active'); } }); } }; // ==================== Table of Contents ==================== const TOCManager = { init() { this.tocLinks = document.querySelectorAll('.toc-list a'); this.headings = []; if (this.tocLinks.length === 0) return; // Collect headings this.tocLinks.forEach(link => { const id = link.getAttribute('href')?.slice(1); const heading = document.getElementById(id); if (heading) { this.headings.push({ id, element: heading, link }); } }); // Scroll spy this.observer = new IntersectionObserver( (entries) => this.handleIntersection(entries), { rootMargin: '-80px 0px -80% 0px' } ); this.headings.forEach(({ element }) => { this.observer.observe(element); }); // Smooth scroll this.tocLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const id = link.getAttribute('href')?.slice(1); const target = document.getElementById(id); if (target) { target.scrollIntoView({ behavior: 'smooth' }); } }); }); }, handleIntersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { // Remove all active states this.tocLinks.forEach(link => link.classList.remove('active')); // Add active to current const activeLink = this.headings.find(h => h.id === entry.target.id)?.link; activeLink?.classList.add('active'); } }); } }; // ==================== Code Copy ==================== window.copyCode = function(button) { const pre = button.closest('pre'); const code = pre?.querySelector('code'); if (!code) return; const text = code.textContent; navigator.clipboard.writeText(text).then(() => { const originalText = button.innerHTML; button.innerHTML = ` 已复制 `; button.style.color = '#22c55e'; setTimeout(() => { button.innerHTML = originalText; button.style.color = ''; }, 2000); }).catch(err => { console.error('Failed to copy:', err); }); }; // ==================== Reading Progress ==================== const ReadingProgress = { init() { this.progressBar = document.createElement('div'); this.progressBar.style.cssText = ` position: fixed; top: 0; left: 0; width: 0%; height: 3px; background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); z-index: 9999; transition: width 0.1s ease; `; document.body.appendChild(this.progressBar); window.addEventListener('scroll', () => this.update()); }, update() { const scrollTop = window.scrollY; const docHeight = document.documentElement.scrollHeight - window.innerHeight; const progress = (scrollTop / docHeight) * 100; this.progressBar.style.width = `${Math.min(progress, 100)}%`; } }; // ==================== Lazy Load Images ==================== const LazyLoad = { init() { if ('loading' in HTMLImageElement.prototype) { // Native lazy loading supported document.querySelectorAll('img[loading="lazy"]').forEach(img => { img.src = img.dataset.src; }); } else { // Fallback with IntersectionObserver this.observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; this.observer.unobserve(img); } }); }, { rootMargin: '50px' } ); document.querySelectorAll('img[data-src]').forEach(img => { this.observer.observe(img); }); } } }; // ==================== Copy Link ==================== window.copyLink = function() { const url = window.location.href; navigator.clipboard.writeText(url).then(() => { const btn = document.querySelector('.share-copy'); btn.classList.add('copied'); btn.title = '已复制'; setTimeout(() => { btn.classList.remove('copied'); btn.title = '复制链接'; }, 2000); }); }; // ==================== Back to Top ==================== const BackToTop = { init() { this.button = document.createElement('button'); this.button.innerHTML = ` `; this.button.style.cssText = ` position: fixed; bottom: 24px; right: 24px; width: 44px; height: 44px; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; visibility: hidden; transition: all 0.3s ease; z-index: 100; color: var(--text-secondary); box-shadow: var(--shadow-md); `; this.button.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); document.body.appendChild(this.button); window.addEventListener('scroll', () => this.toggle()); }, toggle() { if (window.scrollY > 300) { this.button.style.opacity = '1'; this.button.style.visibility = 'visible'; } else { this.button.style.opacity = '0'; this.button.style.visibility = 'hidden'; } } }; // ==================== Initialize ==================== document.addEventListener('DOMContentLoaded', () => { ThemeManager.init(); SearchManager.init(); MobileMenu.init(); TOCManager.init(); ReadingProgress.init(); LazyLoad.init(); BackToTop.init(); // Add current page class to nav const currentPath = window.location.pathname; document.querySelectorAll('.main-nav a').forEach(link => { if (link.pathname === currentPath) { link.classList.add('current'); } }); }); })();