Files
blog/themes/Ying/assets/js/modules/floating-tools.js
T
zqlit 215929a67f feat(comments): 匿名评论 + 表情最近使用;fix: 自动填充聚焦提示 & 邮件模板回退原版 & 返回顶部改 CSS 动画
- 匿名评论:comments.is_anonymous 列;对外昵称/头像/网址/徽标/等级/IP归属全部脱敏,
  真实身份留 users 表(回复通知照发、autofill 不受污染);邮件昵称也用「匿名」防泄露
- 表情包最近使用:面板顶部「最近」栏,localStorage 记录 8 个,零侵入复用 Artalk 插入逻辑
- 匿名开关插在 textarea 与底栏之间独立一行,不挤移动端底栏
- 自动填充:提示改 focusin 触发(原 input+sessionStorage 一次性门导致永不触发);
  已填昵称/网址时不覆盖并弹提示
- 邮件模板 UI 回退原版深色气泡设计,保留表情内联尺寸/主题文案等逻辑修复
- 返回顶部:rAF 自绘改 CSS scroll-behavior(滚动期临时挂类),修移动端掉帧
2026-10-05 09:02:23 +08:00

295 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 平滑滚动 —— 纯 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);
}
};