123 lines
4.4 KiB
JavaScript
123 lines
4.4 KiB
JavaScript
/**
|
||
* 门户滚动工具集
|
||
*
|
||
* 设计原则(参考 Material Design 3 / iOS Safari 滚动模型 / Nuxt 3 page transition):
|
||
* 1. **绝不劫持 wheel 事件** —— 浏览器原生滚动在 GPU 合成、滚动惯性、
|
||
* 触控板缩放、辅助滚动、屏幕阅读器等场景下都是最优的。手写 wheel handler
|
||
* 累加 deltaY + setTimeout 锁会导致"滑动不稳 / 向下滑却向上滚"。
|
||
* 2. **分屏落点用 scrollIntoView + scrollend 判定** —— 让浏览器原生动画,
|
||
* 只在 scrollend 时解锁 / 计算下一段。
|
||
* 3. **CSS overscroll-behavior: contain 隔离滚动冒泡** —— 子容器滚动到边界
|
||
* 时不再触发父级连锁滚动。
|
||
* 4. **deltaMode 区分鼠标/触控板** —— 阈值判断用 pixel 模式。
|
||
*/
|
||
|
||
const SCROLL_END_TIMEOUT_MS = 120;
|
||
|
||
/**
|
||
* 桌面端全屏分屏滚轮(兼容旧调用方;新实现改为原生滚动 + scrollIntoView)
|
||
* 现仅保留接口签名供外部 import,不主动 bind wheel。
|
||
*/
|
||
export function shouldUseSectionWheelScroll() {
|
||
if (typeof window === 'undefined') return true;
|
||
return !window.matchMedia('(max-width: 767px), (pointer: coarse)').matches;
|
||
}
|
||
|
||
/**
|
||
* @deprecated 老接口:不再劫持 wheel 事件。新逻辑请直接用 scrollIntoView + scrollend。
|
||
* 此函数保留仅为不破坏外部 import,运行时不绑定任何监听。
|
||
*/
|
||
export function bindSectionWheelScroll(root, handler) {
|
||
// 静默 no-op,提示调用方迁移
|
||
if (typeof console !== 'undefined') {
|
||
// eslint-disable-next-line no-console
|
||
console.warn(
|
||
'[portal-scroll-mode] bindSectionWheelScroll 已废弃:不再劫持 wheel 事件。'
|
||
+ ' 请改用 scrollIntoView({ behavior: "smooth" }) + scrollend 判定。',
|
||
);
|
||
}
|
||
return () => {};
|
||
}
|
||
|
||
/**
|
||
* 门户主内容区滚回顶部(分页、筛选等页内操作)
|
||
* @param {{ smooth?: boolean, selector?: string }} [options]
|
||
*/
|
||
export function scrollPortalContentToTop(options = {}) {
|
||
if (typeof window === 'undefined') return;
|
||
const { smooth = true, selector = '.content-wrap' } = options;
|
||
const root = document.querySelector(selector);
|
||
const behavior = smooth ? 'smooth' : 'auto';
|
||
if (root) {
|
||
root.scrollTo({ top: 0, behavior });
|
||
return;
|
||
}
|
||
window.scrollTo({ top: 0, behavior });
|
||
}
|
||
|
||
/**
|
||
* Promise 化的 scrollend —— 等到滚动真正结束再 resolve。
|
||
* 兼容不支持 scrollend 的浏览器(Safari < 15.4),fallback 到 debounced scroll。
|
||
* @param {HTMLElement | Window} target
|
||
* @param {number} timeoutMs
|
||
* @returns {Promise<void>}
|
||
*/
|
||
export function waitForScrollEnd(target = window, timeoutMs = SCROLL_END_TIMEOUT_MS) {
|
||
return new Promise((resolve) => {
|
||
if (typeof window === 'undefined') {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
let settled = false;
|
||
const done = () => {
|
||
if (settled) return;
|
||
settled = true;
|
||
target.removeEventListener && target.removeEventListener('scrollend', done);
|
||
target.removeEventListener && target.removeEventListener('scroll', onScroll);
|
||
resolve();
|
||
};
|
||
|
||
const onScroll = () => {
|
||
if (settled) return;
|
||
if (debounceTimer) clearTimeout(debounceTimer);
|
||
debounceTimer = setTimeout(done, timeoutMs);
|
||
};
|
||
let debounceTimer = setTimeout(done, timeoutMs);
|
||
|
||
if ('onscrollend' in window) {
|
||
target.addEventListener('scrollend', done, { once: true, passive: true });
|
||
} else {
|
||
target.addEventListener('scroll', onScroll, { passive: true });
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 平滑滚动到指定 section 元素,等待滚动结束。
|
||
* @param {HTMLElement} el 目标元素
|
||
* @param {{ block?: ScrollLogicalPosition, offset?: number, scrollRoot?: HTMLElement | Window }} [options]
|
||
* @returns {Promise<void>}
|
||
*/
|
||
export async function smoothScrollTo(el, options = {}) {
|
||
if (!el) return;
|
||
const { block = 'start', offset = 0, scrollRoot = window } = options;
|
||
const behavior = 'smooth';
|
||
// 处理 offset:scrollIntoView 不支持 offset,临时用 transform 占位再回滚
|
||
if (offset !== 0) {
|
||
const orig = el.style.scrollMarginTop;
|
||
el.style.scrollMarginTop = `${offset}px`;
|
||
try {
|
||
el.scrollIntoView({ behavior, block, inline: 'nearest' });
|
||
} finally {
|
||
// 还原(nextTick 后给浏览器时间生效)
|
||
setTimeout(() => {
|
||
el.style.scrollMarginTop = orig;
|
||
}, 0);
|
||
}
|
||
} else {
|
||
el.scrollIntoView({ behavior, block, inline: 'nearest' });
|
||
}
|
||
await waitForScrollEnd(scrollRoot);
|
||
}
|