dootask/resources/assets/js/utils/scrollToStableTarget.js
kuaifan ffbb45708d feat(project): 支持表格分组配置与悬停筛选菜单
新增到期、排期、逾期与未排期分组,按项目缓存显示选择并至少保留一项。

统一分组复选下拉菜单,接入 Cascader 桌面悬停展开,增加勾选后的滚动定位校正。

升级 view-design-hi 至 4.7.0-81,同步多语言、知识库和回归测试。
2026-09-07 12:08:52 +00:00

61 lines
2.8 KiB
JavaScript
Vendored

// Lazy-rendered rows can move a target after the browser has started scrolling.
export function scrollToStableTarget({container, target, inputRoot = container, render, isActive}) {
let active = true;
let timer;
let deadline;
const startedAt = Date.now();
let stableSince = startedAt;
let previousTop = null;
let previousHeight = null;
const document = container.ownerDocument;
const desiredTop = () => Math.max(0, Math.min(
container.scrollHeight - container.clientHeight,
container.scrollTop + target.getBoundingClientRect().top - container.getBoundingClientRect().top - container.clientTop,
));
const move = (top, behavior) => container.scrollTo({top, left: container.scrollLeft, behavior});
const valid = () => active && container.isConnected && target.isConnected && isActive();
const finish = (stopMotion = false) => {
if (!active) return;
active = false;
clearTimeout(timer);
clearTimeout(deadline);
for (const event of ['wheel', 'touchstart', 'pointerdown']) inputRoot.removeEventListener(event, cancel, true);
document.removeEventListener('keydown', onKeyDown, true);
if (stopMotion && container.isConnected) move(container.scrollTop, 'instant');
};
const cancel = () => finish(true);
const onKeyDown = event => {
if (event.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
if (['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' '].includes(event.key)) cancel();
};
const tick = async () => {
if (!valid()) return cancel();
try {
await render();
if (!valid()) return cancel();
const now = Date.now();
const top = desiredTop();
const changed = previousTop === null || Math.abs(top - previousTop) > 1 || container.scrollHeight !== previousHeight;
const aligned = Math.abs(container.scrollTop - top) <= 1;
if (changed || !aligned) stableSince = now;
previousTop = top;
previousHeight = container.scrollHeight;
// Give the first smooth scroll time to finish; corrections must not start another animation.
if (!aligned && now - startedAt >= 600) move(top, 'instant');
if (aligned && now - startedAt >= 600 && now - stableSince >= 240) return finish();
timer = setTimeout(tick, 80);
} catch {
cancel();
}
};
if (!valid()) return cancel;
for (const event of ['wheel', 'touchstart', 'pointerdown']) inputRoot.addEventListener(event, cancel, {capture: true, passive: true});
document.addEventListener('keydown', onKeyDown, true);
deadline = setTimeout(cancel, 2500);
move(desiredTop(), 'smooth');
timer = setTimeout(tick, 80);
return cancel;
}