fix(embed): scroll freeze — pointer pause leaked when release happened outside window
Root cause: pointerup/pointercancel bound only on the viewport element; press on a photo then release outside the 195x280 window (or gesture interruption) never cleared the 'pointer' pause reason -> auto-scroll stopped permanently until page refresh. - pointerdown now setPointerCapture (release guaranteed to return) - window-level pointerup/pointercancel + blur fallbacks clear the reason - rAF frame wrapped in try/catch so a single bad frame can no longer break the animation chain Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -98,21 +98,28 @@ export function useVerticalPhotoStream(options: UseVerticalPhotoStreamOptions) {
|
||||
|
||||
function frame(now: number): void {
|
||||
rafId = null;
|
||||
const s = state.value;
|
||||
if (s === null || !scrollable()) return;
|
||||
if (lastTime === null) {
|
||||
lastTime = now;
|
||||
} else {
|
||||
const dt = now - lastTime;
|
||||
lastTime = now;
|
||||
if (dt > 0) {
|
||||
const result = advanceStream(s, dt, speedPxPerSec.value, innerWidth, viewportHeight);
|
||||
if (result.retired) {
|
||||
items.value = s.items.map((item) => ({ ...item }));
|
||||
// 异常保护(滚动卡死修复 2026-09-23):帧回调内任何异常若任其冒泡,
|
||||
// 本帧不会排下一帧 → rAF 链永久断裂(表现为滚动停死、刷新才恢复)。
|
||||
// 逐帧 try-catch,异常帧跳过推进、继续排帧(写 transform 的 DOM 部分同护)。
|
||||
try {
|
||||
const s = state.value;
|
||||
if (s === null || !scrollable()) return;
|
||||
if (lastTime === null) {
|
||||
lastTime = now;
|
||||
} else {
|
||||
const dt = now - lastTime;
|
||||
lastTime = now;
|
||||
if (dt > 0) {
|
||||
const result = advanceStream(s, dt, speedPxPerSec.value, innerWidth, viewportHeight);
|
||||
if (result.retired) {
|
||||
items.value = s.items.map((item) => ({ ...item }));
|
||||
}
|
||||
offsetView.value = s.offset;
|
||||
writeTransform();
|
||||
}
|
||||
offsetView.value = s.offset;
|
||||
writeTransform();
|
||||
}
|
||||
} catch {
|
||||
/* 异常帧:跳过本次推进,链继续(数据边缘态由下次 reanchor 修复) */
|
||||
}
|
||||
if (running.value) {
|
||||
rafId = requestAnimationFrame(frame);
|
||||
|
||||
@@ -219,6 +219,14 @@ function onPointerDown(event: PointerEvent): void {
|
||||
const anchor = (event.target as HTMLElement).closest<HTMLElement>('a.stream-photo');
|
||||
lockedTarget = anchor === null ? null : (anchor.dataset.photoId ?? null);
|
||||
setReason('pointer', true);
|
||||
// 指针捕获(滚动卡死修复 2026-09-23):按下后拖出窗外松开时 viewport 收不到
|
||||
// pointerup,'pointer' 暂停原因会永久残留 → 滚动停死。捕获后 up/cancel 必回
|
||||
// 本元素;window 级兜底监听(onMounted)双保险。
|
||||
try {
|
||||
viewportEl.value?.setPointerCapture(event.pointerId);
|
||||
} catch {
|
||||
/* 指针已释放等:忽略 */
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent): void {
|
||||
@@ -336,8 +344,21 @@ function onFocusOut(): void {
|
||||
}, RESUME_DELAY_MS);
|
||||
}
|
||||
|
||||
// window 级兜底(滚动卡死修复 2026-09-23):pointerup 落在窗外/别的元素、
|
||||
// 或整个 iframe 失焦(弹框抢焦点)时也要清 pointer 原因,避免暂停态永久卡住。
|
||||
function onWindowPointerEnd(): void {
|
||||
if (pointerDownY !== null) onPointerUp();
|
||||
}
|
||||
function onWindowBlur(): void {
|
||||
pointerDownY = null;
|
||||
setReason('pointer', false);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
window.addEventListener('pointerup', onWindowPointerEnd);
|
||||
window.addEventListener('pointercancel', onWindowPointerEnd);
|
||||
window.addEventListener('blur', onWindowBlur);
|
||||
onVisibilityChange();
|
||||
if (typeof IntersectionObserver !== 'undefined' && viewportEl.value !== null) {
|
||||
visibilityObserver = new IntersectionObserver(
|
||||
@@ -354,6 +375,9 @@ onBeforeUnmount(() => {
|
||||
setReason('destroyed', true);
|
||||
layerEl.value?.clear();
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
window.removeEventListener('pointerup', onWindowPointerEnd);
|
||||
window.removeEventListener('pointercancel', onWindowPointerEnd);
|
||||
window.removeEventListener('blur', onWindowBlur);
|
||||
visibilityObserver?.disconnect();
|
||||
if (resumeTimer !== null) clearTimeout(resumeTimer);
|
||||
if (manualClearTimer !== null) clearTimeout(manualClearTimer);
|
||||
|
||||
Reference in New Issue
Block a user