viewer: เปลี่ยนตัวเลื่อนของหน้า annotate จาก window มาเป็น <main> ของเอกสาร — zoom เกินจอแล้วเลื่อนได้ทั้งสองแกน sidebar/header อยู่กับที่ (คำขอผู้ใช้ 2026-09-22: เดิมเลื่อน/ซูมทีเดียวเป็นทั้งหน้าเว็บรวม sidebar ไปด้วย)

- PDFViewer root เป็น app shell flex h-dvh overflow-hidden (dvh กันปัญหา URL bar ของจอสัมผัส) + content column เป็น flex-col + <main> ได้ class pdf-scroll-main min-h-0 flex-1 overflow-auto overscroll-contain = ตัวเลื่อนเดียวของหน้า; aside เปลี่ยน h-screen เป็น h-full (ถอด sticky top-0 ที่เป็น no-op แล้ว เหลือ top-0 สำหรับ max-lg:fixed)
- class ใหม่ .pdf-scroll-main ใน globals.css: touch-action pan-x pan-y บนพื้นที่เอกสาร — กัน browser pinch/double-tap zoom ทั้งหน้าเว็บ (อาการ sidebar โต้ตามตอนซูมบน tablet) วางก่อน .pdf-custom-select เพื่อให้โหมดมาร์กข้อความ (touch-action:none) ยังชนะตอนเปิดใช้; ปุ่มซูม +- เป็นทางเลือก single-pointer ตาม WCAG 2.5.1
- retarget scroll ทุกจุดจาก window มาที่ mainRef.current: scrollToBelowHeader รับ main + helper ใหม่ contentTopInScroll(main, clientTop) แทนสูตร r.top + window.scrollY (ลบ viewerHeaderHeight ที่ไม่จำเป็น — header อยู่นอกกรอบ scroll), ตรวจหน้าปัจจุบันย้าย listener มาที่ main, IO ของ virtualization ตั้ง root เป็น main, deep link ?page/?ann, goToPage/goToBookmark/goToAnnotation/currentViewPosition, tool pan ใช้ main.scrollBy (ลากได้ทั้งแนวนอนด้วย)
- useCustomSelection auto-scroll: เลื่อน scroll container ของ <main> แทน window + เพิ่มแกนนอน (ลากชิดขอบซ้าย/ขวาเลื่อนต่อ — แก้ zoom แล้วเข้าไม่ถึงครึ่งขวาของหน้าในโหมดมาร์กข้อความบน tablet) และขอบวัดจาก rect ของ main (ใต้ header) แทนขอบจอที่โดน header บัง + hint เปลี่ยนเป็น "ลากชิดขอบจอเพื่อเลื่อนหน้า"
- sync กติกา .claude/rules/pdf-viewer.md: เพิ่ม bullet scroll architecture (ห้ามกลับไปเลื่อนที่ window) + แก้คำอธิบาย pan/auto-scroll ที่อ้าง window เดิม

verify: npx tsc --noEmit ผ่าน, vitest 139/139 ผ่าน — การวาด/สไตลัส/pressure และพิกัด % ไม่ถูกแตะ

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-22 09:57:55 +07:00
parent f7b5a8488d
commit f7cd4b8312
4 changed files with 96 additions and 64 deletions

View file

@ -25,7 +25,7 @@ const MOVE_THRESHOLD_PX = 8;
/** อ่าน caret จากจุด y ตรงนิ้วเลื่อนขึ้น 12px — นิ้วบังข้อความที่กำลังเลือก
* (เฉพาะ pointerType touch; เมาส์/สไตลัสชี้แม่นอยู่แล้ว) */
const FINGER_OFFSET_PX = 12;
/** นิ้วอยู่ใกล้ขอบบน/ล่างของ viewport แค่นี้ = auto-scroll ต่อ (rAF loop) */
/** นิ้วอยู่ใกล้ขอบของพื้นที่เอกสาร (ทั้ง 4 ด้าน) แค่นี้ = auto-scroll ต่อ (rAF loop) */
const AUTOSCROLL_MARGIN_PX = 60;
const AUTOSCROLL_MAX_SPEED_PX = 12;
@ -503,35 +503,39 @@ export function useCustomSelection({
});
};
/** auto-scroll ระหว่างลากค้างใกล้ขอบจอ — rAF loop เลื่อน window แล้วคำนวณ preview
* ใหม่ (จุดนิ้วเดิมชี้ข้อความใหม่หลัง scroll) จบเองเมื่อเลิกลาก */
/** auto-scroll ระหว่างลากค้างใกล้ขอบจอ — rAF loop เลื่อน scroll container ของ
* เอกสาร (el = <main> ตัวเลื่อนเดียวของ viewer, ทั้งแกนตั้งและแกนนอนกรณี
* ซูมจนหน้ากว้างเกินจอ) แล้วคำนวณ preview ใหม่ (จุดนิ้วเดิมชี้ข้อความใหม่หลัง
* scroll) จบเองเมื่อเลิกลาก — ขอบวัดจากกรอบของ el เอง (ใต้ header) ไม่ใช่
* ขอบจอ เพราะส่วนบนถูก header บังอยู่ */
const startAutoScroll = () => {
if (scrollRafRef.current !== null) return;
const step = () => {
scrollRafRef.current = null;
const d = dragRef.current;
if (!d?.active) return;
const vh = window.innerHeight;
const br = el.getBoundingClientRect();
const speed = (into: number) =>
Math.max(
1,
Math.ceil(
(into / AUTOSCROLL_MARGIN_PX) * AUTOSCROLL_MAX_SPEED_PX,
),
);
let dx = 0;
let dy = 0;
if (d.curPt.y < AUTOSCROLL_MARGIN_PX) {
dy = -Math.max(
1,
Math.ceil(
((AUTOSCROLL_MARGIN_PX - d.curPt.y) / AUTOSCROLL_MARGIN_PX) *
AUTOSCROLL_MAX_SPEED_PX,
),
);
} else if (d.curPt.y > vh - AUTOSCROLL_MARGIN_PX) {
dy = Math.max(
1,
Math.ceil(
((d.curPt.y - (vh - AUTOSCROLL_MARGIN_PX)) / AUTOSCROLL_MARGIN_PX) *
AUTOSCROLL_MAX_SPEED_PX,
),
);
if (d.curPt.x < br.left + AUTOSCROLL_MARGIN_PX) {
dx = -speed(br.left + AUTOSCROLL_MARGIN_PX - d.curPt.x);
} else if (d.curPt.x > br.right - AUTOSCROLL_MARGIN_PX) {
dx = speed(d.curPt.x - (br.right - AUTOSCROLL_MARGIN_PX));
}
if (dy !== 0) {
window.scrollBy(0, dy);
if (d.curPt.y < br.top + AUTOSCROLL_MARGIN_PX) {
dy = -speed(br.top + AUTOSCROLL_MARGIN_PX - d.curPt.y);
} else if (d.curPt.y > br.bottom - AUTOSCROLL_MARGIN_PX) {
dy = speed(d.curPt.y - (br.bottom - AUTOSCROLL_MARGIN_PX));
}
if (dx !== 0 || dy !== 0) {
el.scrollBy(dx, dy);
schedulePreview();
}
scrollRafRef.current = requestAnimationFrame(step);