360 lines
9.9 KiB
JavaScript
360 lines
9.9 KiB
JavaScript
/**
|
||
* 强力滚动保护脚本
|
||
* 通过劫持 window.scrollTo 和相关滚动方法来阻止意外的滚动跳转
|
||
* 专门解决 Twikoo 评论系统的滚动问题
|
||
*/
|
||
|
||
(() => {
|
||
// 保存原始的滚动方法
|
||
const originalScrollTo = window.scrollTo;
|
||
const originalScrollBy = window.scrollBy;
|
||
const originalScrollIntoView = Element.prototype.scrollIntoView;
|
||
|
||
// 滚动保护状态
|
||
const scrollProtection = {
|
||
enabled: false,
|
||
allowedY: null,
|
||
startTime: 0,
|
||
duration: 0,
|
||
timeout: null,
|
||
};
|
||
|
||
// 检测是否为TOC导航触发的滚动
|
||
function checkIsTOCNavigation() {
|
||
// 检查调用堆栈,看是否来自TOC组件
|
||
const stack = new Error().stack;
|
||
if (
|
||
stack &&
|
||
(stack.includes("handleAnchorClick") || stack.includes("TOC.astro"))
|
||
) {
|
||
return true;
|
||
}
|
||
|
||
// 检查最近是否有TOC点击事件
|
||
if (
|
||
window.tocClickTimestamp &&
|
||
Date.now() - window.tocClickTimestamp < 1000
|
||
) {
|
||
return true;
|
||
}
|
||
|
||
// 检查是否在TOC元素上
|
||
const activeElement = document.activeElement;
|
||
if (activeElement && activeElement.closest("#toc, .table-of-contents")) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// 启动滚动保护
|
||
function enableScrollProtection(duration = 3000, currentY = null) {
|
||
scrollProtection.enabled = true;
|
||
scrollProtection.allowedY =
|
||
currentY !== null ? currentY : window.scrollY || window.pageYOffset;
|
||
scrollProtection.startTime = Date.now();
|
||
scrollProtection.duration = duration;
|
||
|
||
// 清除之前的定时器
|
||
if (scrollProtection.timeout) {
|
||
clearTimeout(scrollProtection.timeout);
|
||
}
|
||
|
||
// 设置保护结束时间
|
||
scrollProtection.timeout = setTimeout(() => {
|
||
scrollProtection.enabled = false;
|
||
console.log("[强力滚动保护] 保护期结束");
|
||
}, duration);
|
||
|
||
console.log(
|
||
`[强力滚动保护] 启动保护 ${duration}ms,允许Y位置:`,
|
||
scrollProtection.allowedY,
|
||
);
|
||
}
|
||
|
||
// 检查滚动是否被允许
|
||
function isScrollAllowed(x, y) {
|
||
if (!scrollProtection.enabled) {
|
||
return true;
|
||
}
|
||
|
||
// 检查是否是TOC或MD导航触发的滚动
|
||
const isTOCNavigation = checkIsTOCNavigation();
|
||
if (isTOCNavigation) {
|
||
console.log("[强力滚动保护] 检测到TOC导航,允许滚动");
|
||
return true;
|
||
}
|
||
|
||
// 允许小幅度的滚动调整(±50像素)
|
||
const tolerance = 50;
|
||
const allowedY = scrollProtection.allowedY;
|
||
|
||
if (Math.abs(y - allowedY) <= tolerance) {
|
||
return true;
|
||
}
|
||
|
||
// 如果尝试滚动到顶部(y < 100)而当前位置在更下方,阻止
|
||
if (y < 100 && allowedY > 100) {
|
||
console.log(
|
||
"[强力滚动保护] 阻止滚动到顶部,目标Y:",
|
||
y,
|
||
"允许Y:",
|
||
allowedY,
|
||
);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// 劫持 window.scrollTo
|
||
window.scrollTo = (x, y) => {
|
||
// 处理参数为对象的情况
|
||
if (typeof x === "object") {
|
||
const options = x;
|
||
x = options.left || 0;
|
||
y = options.top || 0;
|
||
}
|
||
|
||
if (isScrollAllowed(x, y)) {
|
||
originalScrollTo.call(window, x, y);
|
||
} else {
|
||
console.log("[强力滚动保护] 阻止 scrollTo:", x, y);
|
||
// 如果被阻止,滚动到允许的位置
|
||
originalScrollTo.call(window, x, scrollProtection.allowedY);
|
||
}
|
||
};
|
||
|
||
// 劫持 window.scrollBy
|
||
window.scrollBy = (x, y) => {
|
||
const currentY = window.scrollY || window.pageYOffset;
|
||
const targetY = currentY + y;
|
||
|
||
if (typeof x === "object") {
|
||
const options = x;
|
||
x = options.left || 0;
|
||
y = options.top || 0;
|
||
}
|
||
|
||
if (isScrollAllowed(x, targetY)) {
|
||
originalScrollBy.call(window, x, y);
|
||
} else {
|
||
console.log("[强力滚动保护] 阻止 scrollBy:", x, y);
|
||
}
|
||
};
|
||
|
||
// 劫持 Element.scrollIntoView
|
||
Element.prototype.scrollIntoView = function (options) {
|
||
if (!scrollProtection.enabled) {
|
||
originalScrollIntoView.call(this, options);
|
||
return;
|
||
}
|
||
|
||
// 在保护期内,尝试阻止 scrollIntoView
|
||
const rect = this.getBoundingClientRect();
|
||
const currentY = window.scrollY || window.pageYOffset;
|
||
const targetY = currentY + rect.top;
|
||
|
||
if (isScrollAllowed(0, targetY)) {
|
||
originalScrollIntoView.call(this, options);
|
||
} else {
|
||
console.log("[强力滚动保护] 阻止 scrollIntoView");
|
||
}
|
||
};
|
||
|
||
// 监听 Twikoo 相关的交互事件
|
||
document.addEventListener(
|
||
"click",
|
||
(event) => {
|
||
const target = event.target;
|
||
|
||
// 检查是否点击了TOC导航
|
||
if (
|
||
target.closest("#toc, .table-of-contents") &&
|
||
target.closest('a[href^="#"]')
|
||
) {
|
||
window.tocClickTimestamp = Date.now();
|
||
console.log("[强力滚动保护] 检测到TOC导航点击");
|
||
return; // 不启动保护,允许TOC正常工作
|
||
}
|
||
|
||
// 检查是否点击了 Twikoo 相关元素
|
||
if (
|
||
target.closest("#tcomment") ||
|
||
target.matches(
|
||
".tk-action-icon, .tk-submit, .tk-cancel, .tk-preview, .tk-owo, .tk-admin, .tk-edit, .tk-delete, .tk-reply, .tk-expand",
|
||
) ||
|
||
target.closest(
|
||
".tk-action-icon, .tk-submit, .tk-cancel, .tk-preview, .tk-owo, .tk-admin, .tk-edit, .tk-delete, .tk-reply, .tk-expand",
|
||
)
|
||
) {
|
||
// 立即启动保护
|
||
enableScrollProtection(4000); // 增加保护时间到4秒
|
||
console.log("[强力滚动保护] 检测到 Twikoo 交互,启动保护");
|
||
}
|
||
|
||
// 特别检查管理面板相关操作(包括关闭操作)
|
||
if (
|
||
target.matches(
|
||
".tk-admin-panel, .tk-admin-overlay, .tk-modal, .tk-dialog, .tk-admin-close, .tk-close",
|
||
) ||
|
||
target.closest(
|
||
".tk-admin-panel, .tk-admin-overlay, .tk-modal, .tk-dialog, .tk-admin-close, .tk-close",
|
||
) ||
|
||
target.classList.contains("tk-admin") ||
|
||
target.closest(".tk-admin")
|
||
) {
|
||
enableScrollProtection(6000); // 管理面板操作保护更长时间
|
||
console.log("[强力滚动保护] 检测到 Twikoo 管理面板操作,启动长期保护");
|
||
}
|
||
|
||
// 检查是否点击了遮罩层(通常用于关闭模态框)
|
||
if (
|
||
target.classList.contains("tk-overlay") ||
|
||
target.classList.contains("tk-mask") ||
|
||
target.matches('[class*="overlay"]') ||
|
||
target.matches('[class*="mask"]') ||
|
||
target.matches('[class*="backdrop"]')
|
||
) {
|
||
// 检查是否在 Twikoo 区域内
|
||
const tcommentEl = document.querySelector("#tcomment");
|
||
if (
|
||
tcommentEl &&
|
||
(target.closest("#tcomment") || tcommentEl.contains(target))
|
||
) {
|
||
enableScrollProtection(4000);
|
||
console.log("[强力滚动保护] 检测到 Twikoo 遮罩层点击,启动保护");
|
||
}
|
||
}
|
||
},
|
||
true,
|
||
); // 使用捕获阶段
|
||
|
||
// 监听表单提交
|
||
document.addEventListener(
|
||
"submit",
|
||
(event) => {
|
||
if (event.target.closest("#tcomment")) {
|
||
enableScrollProtection(4000);
|
||
console.log("[强力滚动保护] 检测到 Twikoo 表单提交,启动保护");
|
||
}
|
||
},
|
||
true,
|
||
);
|
||
|
||
// 监听键盘事件(特别是 ESC 键,用于关闭管理面板)
|
||
document.addEventListener(
|
||
"keydown",
|
||
(event) => {
|
||
if (event.key === "Escape" || event.keyCode === 27) {
|
||
// 检查是否在 Twikoo 区域内有活动的管理面板
|
||
const tcommentEl = document.querySelector("#tcomment");
|
||
if (tcommentEl) {
|
||
// 检查是否有可见的管理面板或模态框
|
||
const adminPanel = tcommentEl.querySelector(
|
||
".tk-admin-panel, .tk-modal, .tk-dialog, [class*='admin'], [class*='modal']",
|
||
);
|
||
if (adminPanel && adminPanel.offsetParent !== null) {
|
||
// 面板可见,启动保护
|
||
enableScrollProtection(3000);
|
||
console.log(
|
||
"[强力滚动保护] 检测到 ESC 键关闭 Twikoo 管理面板,启动保护",
|
||
);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
true,
|
||
);
|
||
|
||
// 监听 DOM 变化,检测管理面板的关闭
|
||
const observer = new MutationObserver((mutations) => {
|
||
mutations.forEach((mutation) => {
|
||
if (mutation.type === "childList" || mutation.type === "attributes") {
|
||
const target = mutation.target;
|
||
|
||
// 检查是否是 Twikoo 相关的 DOM 变化
|
||
if (target.closest && target.closest("#tcomment")) {
|
||
// 检查是否有元素被移除或隐藏(可能是面板关闭)
|
||
if (
|
||
mutation.removedNodes.length > 0 ||
|
||
(mutation.type === "attributes" &&
|
||
mutation.attributeName === "style")
|
||
) {
|
||
enableScrollProtection(2000);
|
||
console.log(
|
||
"[强力滚动保护] 检测到 Twikoo DOM 变化(可能是面板关闭),启动保护",
|
||
);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// 开始监听 DOM 变化
|
||
if (document.body) {
|
||
observer.observe(document.body, {
|
||
childList: true,
|
||
subtree: true,
|
||
attributes: true,
|
||
attributeFilter: ["style", "class"],
|
||
});
|
||
} else {
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
observer.observe(document.body, {
|
||
childList: true,
|
||
subtree: true,
|
||
attributes: true,
|
||
attributeFilter: ["style", "class"],
|
||
});
|
||
});
|
||
}
|
||
|
||
// 提供全局接口
|
||
window.scrollProtectionManager = {
|
||
enable: enableScrollProtection,
|
||
disable: () => {
|
||
scrollProtection.enabled = false;
|
||
if (scrollProtection.timeout) {
|
||
clearTimeout(scrollProtection.timeout);
|
||
}
|
||
console.log("[强力滚动保护] 手动停止保护");
|
||
},
|
||
isEnabled: () => scrollProtection.enabled,
|
||
getStatus: () => ({ ...scrollProtection }),
|
||
// 新增:强制保护模式(用于调试)
|
||
forceProtect: (duration = 10000) => {
|
||
enableScrollProtection(duration);
|
||
console.log(`[强力滚动保护] 强制保护模式启动 ${duration}ms`);
|
||
},
|
||
// 新增:获取当前滚动位置
|
||
getCurrentScroll: () => {
|
||
return {
|
||
x: window.scrollX || window.pageXOffset,
|
||
y: window.scrollY || window.pageYOffset,
|
||
};
|
||
},
|
||
// 新增:检测 Twikoo 状态
|
||
checkTwikooStatus: () => {
|
||
const tcomment = document.querySelector("#tcomment");
|
||
if (!tcomment) return { exists: false };
|
||
|
||
const adminPanels = tcomment.querySelectorAll(
|
||
".tk-admin-panel, .tk-modal, .tk-dialog, [class*='admin'], [class*='modal']",
|
||
);
|
||
const visiblePanels = Array.from(adminPanels).filter(
|
||
(panel) => panel.offsetParent !== null,
|
||
);
|
||
|
||
return {
|
||
exists: true,
|
||
adminPanelsCount: adminPanels.length,
|
||
visiblePanelsCount: visiblePanels.length,
|
||
hasVisiblePanels: visiblePanels.length > 0,
|
||
};
|
||
},
|
||
};
|
||
|
||
console.log("[强力滚动保护] 初始化完成");
|
||
})();
|