- 匿名评论:comments.is_anonymous 列;对外昵称/头像/网址/徽标/等级/IP归属全部脱敏, 真实身份留 users 表(回复通知照发、autofill 不受污染);邮件昵称也用「匿名」防泄露 - 表情包最近使用:面板顶部「最近」栏,localStorage 记录 8 个,零侵入复用 Artalk 插入逻辑 - 匿名开关插在 textarea 与底栏之间独立一行,不挤移动端底栏 - 自动填充:提示改 focusin 触发(原 input+sessionStorage 一次性门导致永不触发); 已填昵称/网址时不覆盖并弹提示 - 邮件模板 UI 回退原版深色气泡设计,保留表情内联尺寸/主题文案等逻辑修复 - 返回顶部:rAF 自绘改 CSS scroll-behavior(滚动期临时挂类),修移动端掉帧
295 lines
13 KiB
JavaScript
295 lines
13 KiB
JavaScript
// 平滑滚动 —— 纯 CSS 实现(scroll-behavior: smooth)。
|
||
// 之前用 rAF + window.scrollTo 自绘动画:每一帧都在主线程上强制重排重绘,
|
||
// 移动端长页面(尤其带 backdrop-filter / 大图的)掉帧明显,非常卡。
|
||
// CSS 平滑滚动由浏览器合成器接管,不占 JS 主线程,移动端丝滑得多。
|
||
// 做法:只在滚动期间临时给 <html> 挂 .css-smooth-scroll 类,滚完就摘 ——
|
||
// 避免全局常开平滑滚动干扰 PJAX 切页回顶、锚点跳转这些场景。
|
||
window.smoothScrollTo = function(targetY) {
|
||
targetY = Math.max(0, Math.floor(targetY || 0));
|
||
if (Math.abs(window.scrollY - targetY) < 2) return;
|
||
|
||
var root = document.documentElement;
|
||
// 上一次滚动还没收尾就直接再来一次:先摘干净旧状态
|
||
if (window.__smoothScrollCleanup) window.__smoothScrollCleanup();
|
||
|
||
var reduced = false;
|
||
try { reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (e) {}
|
||
if (reduced) { window.scrollTo(0, targetY); return; } // 用户要求减少动态效果:瞬移
|
||
|
||
root.classList.add('css-smooth-scroll');
|
||
window.scrollTo(0, targetY); // 类在位,浏览器自动走平滑滚动
|
||
|
||
var done = function () {
|
||
if (window.__smoothScrollTimer) { clearTimeout(window.__smoothScrollTimer); window.__smoothScrollTimer = null; }
|
||
window.removeEventListener('scrollend', done);
|
||
window.removeEventListener('wheel', done, { passive: true } );
|
||
window.removeEventListener('touchstart', done, { passive: true });
|
||
root.classList.remove('css-smooth-scroll');
|
||
window.__smoothScrollCleanup = null;
|
||
};
|
||
window.__smoothScrollCleanup = done;
|
||
// scrollend(Chrome 114+/FF 109+)在滚动真正停下的瞬间触发;
|
||
// 不支持的浏览器用超时兜底摘类。滚轮/触摸会打断 CSS 平滑滚动,
|
||
// 提前收尾把类摘掉,不影响用户接着手动滚。
|
||
window.addEventListener('scrollend', done, { once: true });
|
||
window.addEventListener('wheel', done, { once: true, passive: true });
|
||
window.addEventListener('touchstart', done, { once: true, passive: true });
|
||
window.__smoothScrollTimer = setTimeout(done, 1000);
|
||
};
|
||
|
||
window.refreshFloatingTools = function() {
|
||
if (typeof window.initFloatingTools !== 'function') return;
|
||
var delays = [0, 60, 180];
|
||
delays.forEach(function(delay) {
|
||
setTimeout(function() {
|
||
requestAnimationFrame(function() {
|
||
try {
|
||
window.initFloatingTools();
|
||
} catch (e) {
|
||
console.error('initFloatingTools error:', e);
|
||
}
|
||
});
|
||
}, delay);
|
||
});
|
||
};
|
||
|
||
window.initFloatingTools = function() {
|
||
try {
|
||
// 0. Global Toolbar Visibility (Homepage Check)
|
||
var container = document.querySelector('.floating-tools');
|
||
|
||
// Robust Homepage Check:
|
||
// 1. Check for Homepage-specific element (.recent-posts)
|
||
// 2. Ensure NOT on a Post page (.post-content)
|
||
// 3. Fallback: Check pathname (only if DOM check is ambiguous)
|
||
var pjaxContainer = document.getElementById('pjax-container');
|
||
var hasRecentPosts = !!(pjaxContainer && pjaxContainer.querySelector('.recent-posts'));
|
||
var hasArticle = !!(pjaxContainer && (pjaxContainer.querySelector('#post-writing') || pjaxContainer.querySelector('#Comments')));
|
||
var path = window.location.pathname;
|
||
var isHomePage = !hasArticle && (hasRecentPosts || path === '/' || path === '/index.html' || path.startsWith('/page/'));
|
||
|
||
if (container) {
|
||
if (isHomePage) {
|
||
container.style.display = 'none';
|
||
} else {
|
||
container.style.display = 'flex';
|
||
container.classList.remove('hidden');
|
||
container.style.visibility = 'visible';
|
||
container.style.opacity = '1';
|
||
container.style.pointerEvents = 'auto';
|
||
}
|
||
}
|
||
|
||
// 1. Back to Top (Optimized with IntersectionObserver)
|
||
var backToTopButton = document.getElementById('back-to-top');
|
||
if (backToTopButton) {
|
||
|
||
// Clean up old scroll handler if exists (from previous Pjax or version)
|
||
if (window.backToTopScrollHandler) {
|
||
window.removeEventListener('scroll', window.backToTopScrollHandler);
|
||
window.backToTopScrollHandler = null;
|
||
}
|
||
|
||
// Create or reuse sentinel for IntersectionObserver
|
||
var sentinelId = 'back-to-top-sentinel';
|
||
var sentinel = document.getElementById(sentinelId);
|
||
if (!sentinel) {
|
||
sentinel = document.createElement('div');
|
||
sentinel.id = sentinelId;
|
||
// Sentinel covers the top 300px. When it exits viewport, we show button.
|
||
sentinel.style.cssText = 'position: absolute; top: 0; left: 0; width: 1px; height: 300px; pointer-events: none; visibility: hidden; z-index: -9999;';
|
||
document.body.appendChild(sentinel);
|
||
}
|
||
|
||
if (window.IntersectionObserver) {
|
||
if (window.backToTopObserver) {
|
||
window.backToTopObserver.disconnect();
|
||
}
|
||
window.backToTopObserver = new IntersectionObserver(function(entries) {
|
||
entries.forEach(function(entry) {
|
||
if (!entry.isIntersecting) {
|
||
backToTopButton.classList.add('show');
|
||
} else {
|
||
backToTopButton.classList.remove('show');
|
||
}
|
||
});
|
||
});
|
||
window.backToTopObserver.observe(sentinel);
|
||
} else {
|
||
// Fallback for very old browsers
|
||
var ticking = false;
|
||
var update = function() {
|
||
if (window.scrollY > 300) {
|
||
backToTopButton.classList.add('show');
|
||
} else {
|
||
backToTopButton.classList.remove('show');
|
||
}
|
||
ticking = false;
|
||
};
|
||
window.backToTopScrollHandler = function() {
|
||
if (!ticking) {
|
||
window.requestAnimationFrame(update);
|
||
ticking = true;
|
||
}
|
||
};
|
||
window.addEventListener('scroll', window.backToTopScrollHandler, { passive: true });
|
||
}
|
||
|
||
// Re-bind click event
|
||
var newBtn = backToTopButton.cloneNode(true);
|
||
if (backToTopButton.parentNode) {
|
||
backToTopButton.parentNode.replaceChild(newBtn, backToTopButton);
|
||
backToTopButton = newBtn;
|
||
}
|
||
|
||
backToTopButton.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
window.smoothScrollTo(0);
|
||
});
|
||
}
|
||
|
||
// 2. Theme Toggle
|
||
var themeToggle = document.getElementById('theme-toggle');
|
||
if (themeToggle) {
|
||
var newToggle = themeToggle.cloneNode(true);
|
||
if (themeToggle.parentNode) {
|
||
themeToggle.parentNode.replaceChild(newToggle, themeToggle);
|
||
themeToggle = newToggle;
|
||
}
|
||
|
||
// Update icon based on current theme
|
||
var updateIcon = function() {
|
||
var currentTheme = document.documentElement.getAttribute('data-theme');
|
||
var icon = themeToggle.querySelector('i');
|
||
if (icon) {
|
||
if (currentTheme === 'dark') {
|
||
icon.className = 'ri-moon-line';
|
||
} else {
|
||
icon.className = 'ri-sun-line';
|
||
}
|
||
}
|
||
};
|
||
updateIcon();
|
||
|
||
themeToggle.addEventListener('click', function(e) {
|
||
var currentTheme = document.documentElement.getAttribute('data-theme');
|
||
var newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||
|
||
if (typeof themeSweep === 'function') {
|
||
var apply = function() {
|
||
document.documentElement.setAttribute('data-theme', newTheme);
|
||
localStorage.setItem('theme', newTheme);
|
||
updateIcon();
|
||
if (window.artalk && typeof window.artalk.setDarkMode === 'function') {
|
||
window.artalk.setDarkMode(newTheme === 'dark');
|
||
}
|
||
if (window.showToast) {
|
||
window.showToast('已切换至 ' + (newTheme === 'dark' ? '深色' : '浅色') + '模式', 'success');
|
||
}
|
||
};
|
||
themeSweep(newTheme, apply);
|
||
} else {
|
||
document.documentElement.setAttribute('data-theme', newTheme);
|
||
localStorage.setItem('theme', newTheme);
|
||
updateIcon();
|
||
if (window.artalk && typeof window.artalk.setDarkMode === 'function') {
|
||
window.artalk.setDarkMode(newTheme === 'dark');
|
||
}
|
||
if (window.showToast) {
|
||
window.showToast('已切换至 ' + (newTheme === 'dark' ? '深色' : '浅色') + '模式', 'success');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 3. To Comments
|
||
var toComments = document.getElementById('to-comments');
|
||
if (toComments) {
|
||
// Logic: Only show if comments section exists
|
||
var checkCommentVisibility = function(btn) {
|
||
if (!btn) return;
|
||
var hasComments = document.getElementById('Comments') || document.querySelector('.artalk');
|
||
|
||
if (hasComments) {
|
||
btn.style.display = ''; // Show
|
||
} else {
|
||
btn.style.display = 'none'; // Hide
|
||
}
|
||
};
|
||
|
||
// Initial check
|
||
// Use setTimeout to avoid initial render flickering
|
||
setTimeout(function() {
|
||
checkCommentVisibility(toComments);
|
||
}, 100);
|
||
|
||
var newToComments = toComments.cloneNode(true);
|
||
if (toComments.parentNode) {
|
||
toComments.parentNode.replaceChild(newToComments, toComments);
|
||
toComments = newToComments;
|
||
}
|
||
|
||
// Re-run check on new element
|
||
checkCommentVisibility(toComments);
|
||
|
||
// Double-check after a delay for PJAX/Async rendering
|
||
setTimeout(function() {
|
||
var currentBtn = document.getElementById('to-comments');
|
||
if (currentBtn) {
|
||
checkCommentVisibility(currentBtn);
|
||
}
|
||
}, 500);
|
||
|
||
toComments.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
var comments = document.getElementById('Comments') || document.querySelector('.artalk');
|
||
if (comments) {
|
||
var y = comments.getBoundingClientRect().top + window.scrollY - 20;
|
||
window.smoothScrollTo(y);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 4. Lightbox Observer (ViewImage)
|
||
// Hide floating tools when lightbox is open
|
||
if (window.lightboxObserver) {
|
||
window.lightboxObserver.disconnect();
|
||
}
|
||
|
||
if (container && !isHomePage) {
|
||
var lightboxToolsHidden = null;
|
||
var syncLightboxState = function() {
|
||
var lightbox = document.querySelector('.view-image');
|
||
var lightboxOpen = false;
|
||
if (lightbox) {
|
||
var rect = lightbox.getBoundingClientRect();
|
||
var style = window.getComputedStyle(lightbox);
|
||
lightboxOpen = style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0;
|
||
}
|
||
if (lightboxToolsHidden === lightboxOpen) return;
|
||
lightboxToolsHidden = lightboxOpen;
|
||
|
||
if (lightboxOpen) {
|
||
container.classList.add('hidden');
|
||
} else {
|
||
container.classList.remove('hidden');
|
||
container.style.display = 'flex';
|
||
container.style.visibility = 'visible';
|
||
container.style.opacity = '1';
|
||
container.style.pointerEvents = 'auto';
|
||
}
|
||
};
|
||
|
||
window.lightboxObserver = new MutationObserver(function() {
|
||
requestAnimationFrame(syncLightboxState);
|
||
});
|
||
|
||
window.lightboxObserver.observe(document.body, { childList: true, subtree: true });
|
||
syncLightboxState();
|
||
}
|
||
|
||
} catch (e) {
|
||
console.error('Error in initFloatingTools:', e);
|
||
}
|
||
};
|