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:
parent
f7b5a8488d
commit
f7cd4b8312
4 changed files with 96 additions and 64 deletions
File diff suppressed because one or more lines are too long
|
|
@ -247,6 +247,16 @@ html {
|
|||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
/* 2026-09-22: <main> ของ viewer เป็น scroll container เดียวของหน้า (แนวตั้ง +
|
||||
แนวนอน — ซูมจนหน้ากว้างเกินจอแล้วปัดเลื่อนดูส่วนที่เกินได้) — จำกัด gesture
|
||||
สัมผัสให้เป็นการแพนของกรอบนี้เท่านั้น ไม่ปล่อยให้ browser pinch-zoom/double-tap
|
||||
zoom ทั้งหน้าเว็บ (อาการซูมแล้ว header/sidebar โต้ตามไปด้วย); ปุ่มซูม ± เป็น
|
||||
ทางเลือก single-pointer ตาม WCAG 2.5.1 — ต้องอยู่ก่อน .pdf-custom-select
|
||||
เพื่อให้โหมดมาร์กข้อความ (touch-action:none) ยังชนะตอนเปิดใช้ */
|
||||
.pdf-scroll-main {
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
/* custom selection ของ tool highlight บนจอสัมผัส (hooks/useCustomSelection) —
|
||||
ปิด native selection/callout ของ OS และยึด gesture ลากเป็นของเราทั้งหมด
|
||||
(touch-action:none กัน browser ยึดไปเป็น scroll → pointercancel)
|
||||
|
|
|
|||
|
|
@ -210,11 +210,6 @@ function normalizeBookmarkTitle(s: string): string {
|
|||
: t;
|
||||
}
|
||||
|
||||
/** ความสูง sticky header ของ viewer — ใช้เว้นระยะเวลาเลื่อนหน้าไม่ให้เป้าหมายถูก header บัง */
|
||||
function viewerHeaderHeight(): number {
|
||||
return document.querySelector("header")?.offsetHeight ?? 0;
|
||||
}
|
||||
|
||||
/** ความกว้างจริงของเมนู dropdown ของ tool (w-60) กับ popup bookmark (w-72) —
|
||||
* ใช้ clamp ตำแหน่ง fixed ให้อยู่ในจอ (C1) */
|
||||
const TOOL_MENU_W = 240;
|
||||
|
|
@ -230,13 +225,14 @@ function fixedMenuPos(rect: DOMRect, menuW: number): { left: number; top: number
|
|||
};
|
||||
}
|
||||
|
||||
/** เลื่อนหน้าต่างให้ตำแหน่ง absolute (px จากบนสุดของ document) ไปอยู่ใต้ sticky header
|
||||
* (เว้นระยะห่างจาก toolbar 40px ให้รายการไม่ติดแถบเครื่องมือเกินไป) */
|
||||
/** เลื่อน scroll container ของเอกสาร (<main> — ตัวเลื่อนเดียวของ viewer) ให้ตำแหน่ง
|
||||
* contentTop (px จากบนสุดของเนื้อหาใน container) ไปอยู่แถวบนสุดของพื้นที่มองเห็น —
|
||||
* header อยู่นอกกรอบ scroll อยู่แล้ว จึงเว้นแค่ช่องว่าง 40px ให้รายการไม่ติดขอบบนเกินไป */
|
||||
const SCROLL_BELOW_HEADER_GAP = 40;
|
||||
|
||||
function scrollToBelowHeader(absoluteTop: number): void {
|
||||
window.scrollTo({
|
||||
top: Math.max(0, absoluteTop - viewerHeaderHeight() - SCROLL_BELOW_HEADER_GAP),
|
||||
function scrollToBelowHeader(main: HTMLElement | null, contentTop: number): void {
|
||||
main?.scrollTo({
|
||||
top: Math.max(0, contentTop - SCROLL_BELOW_HEADER_GAP),
|
||||
// L7 (UX audit 2026-09-16): เคารพ prefers-reduced-motion — คู่กับ rule ใน globals.css
|
||||
behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
? "auto"
|
||||
|
|
@ -244,6 +240,13 @@ function scrollToBelowHeader(absoluteTop: number): void {
|
|||
});
|
||||
}
|
||||
|
||||
/** แปลง client Y (getBoundingClientRect().top) → พิกัดแนวตั้งในเนื้อหาของ scroll
|
||||
* container ของเอกสาร — แทนสูตร r.top + window.scrollY ยุคที่ window เป็นตัวเลื่อน
|
||||
* (app shell สูงจอควบ lock การ scroll ของ document ไว้ ทุกอย่างเลื่อนใน <main>) */
|
||||
function contentTopInScroll(main: HTMLElement, clientTop: number): number {
|
||||
return clientTop - main.getBoundingClientRect().top + main.scrollTop;
|
||||
}
|
||||
|
||||
/** path เส้นขยุกขยิกยาว w px — ใช้ใน highlight-layer สำหรับ style "squiggle" */
|
||||
function squigglePath(w: number): string {
|
||||
let d = "M0 2.5";
|
||||
|
|
@ -1585,6 +1588,8 @@ export default function PDFViewer({
|
|||
}, [file.id, file.pdfUrl]);
|
||||
|
||||
const pageWrapRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
// scroll container ของเอกสาร (<main>) — ตัวเลื่อนเดียวของ viewer (sidebar/header นิ่ง)
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
|
||||
// Page Virtualization: render เฉพาะหน้าใน viewport ± 1 หน้า (invariant #10)
|
||||
const [renderPages, setRenderPages] = useState<Set<number>>(new Set([0]));
|
||||
|
|
@ -1609,7 +1614,8 @@ export default function PDFViewer({
|
|||
}
|
||||
setRenderPages(next);
|
||||
},
|
||||
{ rootMargin: "100% 0px" },
|
||||
// root = scroll container ของเอกสาร ไม่ใช่ viewport (window ไม่ scroll แล้ว)
|
||||
{ root: mainRef.current, rootMargin: "100% 0px" },
|
||||
);
|
||||
for (const el of pageWrapRefs.current) {
|
||||
if (el) io.observe(el);
|
||||
|
|
@ -1621,14 +1627,18 @@ export default function PDFViewer({
|
|||
useEffect(() => {
|
||||
if (pageSizes.length === 0) return;
|
||||
const onScroll = () => {
|
||||
const center = window.scrollY + window.innerHeight / 2;
|
||||
const main = mainRef.current;
|
||||
if (!main) return;
|
||||
const mr = main.getBoundingClientRect();
|
||||
// กึ่งกลางจอ (client) แปลงเป็นพิกัดในเนื้อหาของ <main> เพื่อเทียบกับ midpoint หน้า
|
||||
const center = main.scrollTop + (window.innerHeight / 2 - mr.top);
|
||||
let best = 0;
|
||||
let bestDist = Infinity;
|
||||
for (let i = 0; i < pageWrapRefs.current.length; i++) {
|
||||
const el = pageWrapRefs.current[i];
|
||||
if (!el) continue;
|
||||
const r = el.getBoundingClientRect();
|
||||
const mid = r.top + window.scrollY + r.height / 2;
|
||||
const mid = contentTopInScroll(main, r.top + r.height / 2);
|
||||
const d = Math.abs(mid - center);
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
|
|
@ -1638,8 +1648,8 @@ export default function PDFViewer({
|
|||
setCurrentPage(best);
|
||||
};
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
mainRef.current?.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => mainRef.current?.removeEventListener("scroll", onScroll);
|
||||
}, [pageSizes]);
|
||||
|
||||
// F12/C4: deep link `?page=N` (1-based) และ/หรือ `?ann=<uuid>` — อ่าน
|
||||
|
|
@ -1665,8 +1675,9 @@ export default function PDFViewer({
|
|||
if (targetPage === null && Number.isInteger(pageParam) && pageParam >= 1) {
|
||||
targetPage = Math.min(pageParam - 1, pageSizes.length - 1);
|
||||
const el = pageWrapRefs.current[targetPage];
|
||||
if (el) {
|
||||
scrollToBelowHeader(el.getBoundingClientRect().top + window.scrollY);
|
||||
const main = mainRef.current;
|
||||
if (el && main) {
|
||||
scrollToBelowHeader(main, contentTopInScroll(main, el.getBoundingClientRect().top));
|
||||
}
|
||||
}
|
||||
}, [pageSizes, annotations]);
|
||||
|
|
@ -1674,7 +1685,6 @@ export default function PDFViewer({
|
|||
// จอเล็ก: หน้ากระดาษ (scale 1) กว้างกว่าพื้นที่แสดงผล → ย่อให้พอดีความกว้าง
|
||||
// ครั้งเดียวตอนโหลดเท่านั้น (ผู้ใช้ซูมเองหลังจากนี้แล้วไม่ถูกแทนที่) —
|
||||
// อิงความกว้างหน้าแรกเหมือน basePageWidth และตัด padding px-4 ซ้าย-ขวาของ main ออก
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
const fitWidthDoneRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (pageSizes.length === 0 || fitWidthDoneRef.current) return;
|
||||
|
|
@ -2192,14 +2202,14 @@ export default function PDFViewer({
|
|||
/** ตำแหน่งที่กำลังดู: หน้าปัจจุบัน + จุดกึ่งกลางจอเป็น % ของความสูงหน้า */
|
||||
function currentViewPosition() {
|
||||
const el = pageWrapRefs.current[currentPage];
|
||||
if (!el) return null;
|
||||
const main = mainRef.current;
|
||||
if (!el || !main) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
// นิยาม yOffset แบบเดียวกับพิกัด annotation: "จุดจริงบนหน้า" — ใช้ขอบบนของ
|
||||
// แถบมองเห็นใต้ header (+gap เท่ากับตอนกดผ่าน scrollToBelowHeader) ตอนกด
|
||||
// bookmark จุดนี้จะกลับมาอยู่ตำแหน่งเดิมพอดี ไม่ใช่กึ่งกลางจอแบบเดิม
|
||||
const viewTop =
|
||||
window.scrollY + viewerHeaderHeight() + SCROLL_BELOW_HEADER_GAP;
|
||||
const pageTop = r.top + window.scrollY;
|
||||
// แถบมองเห็นของ scroll container (+gap เท่ากับตอนกดผ่าน scrollToBelowHeader)
|
||||
// ตอนกด bookmark จุดนี้จะกลับมาอยู่ตำแหน่งเดิมพอดี ไม่ใช่กึ่งกลางจอแบบเดิม
|
||||
const viewTop = main.scrollTop + SCROLL_BELOW_HEADER_GAP;
|
||||
const pageTop = contentTopInScroll(main, r.top);
|
||||
return {
|
||||
pageIndex: currentPage,
|
||||
yOffset: clamp(((viewTop - pageTop) / r.height) * 100, 0, 100),
|
||||
|
|
@ -2708,13 +2718,14 @@ export default function PDFViewer({
|
|||
// เห็นเป้าหมายทันที ไม่ต้องปิดเองก่อน (deep link ผ่าน goToAnnotation ก็เช่นกัน)
|
||||
if (!window.matchMedia("(min-width: 1024px)").matches) setSidebarOpen(false);
|
||||
const el = pageWrapRefs.current[b.pageIndex];
|
||||
if (!el) return;
|
||||
const main = mainRef.current;
|
||||
if (!el || !main) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
const pageTop = r.top + window.scrollY;
|
||||
// ดันจุด yOffset กลับมาชิดใต้ sticky header — pattern เดียวกับ goToAnnotation
|
||||
const pageTop = contentTopInScroll(main, r.top);
|
||||
// ดันจุด yOffset กลับมาชิดขอบบนของพื้นที่มองเห็น — pattern เดียวกับ goToAnnotation
|
||||
// ที่ดันขอบบน bbox (yOffset เก็บจาก "ขอบบนของสิ่งที่ดูอยู่" ตอนสร้าง กดแล้วจึงได้
|
||||
// มุมมองเดิมพอดี ไม่ต้องชดเชยครึ่งจอ)
|
||||
scrollToBelowHeader(pageTop + (r.height * b.yOffset) / 100);
|
||||
scrollToBelowHeader(main, pageTop + (r.height * b.yOffset) / 100);
|
||||
}
|
||||
|
||||
function goToAnnotation(a: AnnotationDTO) {
|
||||
|
|
@ -2727,10 +2738,11 @@ export default function PDFViewer({
|
|||
setPreviewData(null);
|
||||
setOpenPinId(null); // กระโดดไปเลือกรายการอื่น = ปิด popover ของ pin เดิมทิ้ง
|
||||
const el = pageWrapRefs.current[a.pageIndex];
|
||||
if (!el) return;
|
||||
const main = mainRef.current;
|
||||
if (!el || !main) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
const boxPct = annotationBBox(a, { width: 100, height: 100 });
|
||||
scrollToBelowHeader(r.top + window.scrollY + (r.height * boxPct.y) / 100);
|
||||
scrollToBelowHeader(main, contentTopInScroll(main, r.top) + (r.height * boxPct.y) / 100);
|
||||
}
|
||||
|
||||
/** คลิก comment pin = เปิด popover + เลือกรายการ (F1) */
|
||||
|
|
@ -2832,9 +2844,10 @@ export default function PDFViewer({
|
|||
* placeholder ทุกหน้ามีขนาดจริงจึงคำนวณ absolute scroll ได้เสมอ) */
|
||||
function goToPage(pageIndex: number) {
|
||||
const el = pageWrapRefs.current[pageIndex];
|
||||
if (!el) return;
|
||||
const main = mainRef.current;
|
||||
if (!el || !main) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
scrollToBelowHeader(r.top + window.scrollY);
|
||||
scrollToBelowHeader(main, contentTopInScroll(main, r.top));
|
||||
}
|
||||
|
||||
// ─── Render ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -2860,7 +2873,10 @@ export default function PDFViewer({
|
|||
const basePageWidth = pageSizes[0]?.width ?? 595;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
// 2026-09-22: app shell สูงจอเดียว (dvh กันปัญหา URL bar ของจอสัมผัส) —
|
||||
// ตัวเลื่อนเดียวของหน้าคือ <main> ของเอกสาร sidebar/header อยู่กับที่
|
||||
// ไม่หนีไปกับ scroll/zoom ตามคำขอผู้ใช้ (เดิมเลื่อนที่ window ทั้งหน้า)
|
||||
<div className="flex h-dvh overflow-hidden">
|
||||
{/* tool คลุมข้อความได้ (TEXT_SELECTION_TOOLS) — ซ่อนสี selection ของเบราว์เซอร์
|
||||
ให้เห็นแต่ preview มาร์ก/selection สีฟ้าของเรา (กรองแนวเส้นทางลากแล้ว
|
||||
สะอาดกว่า native) */}
|
||||
|
|
@ -2876,12 +2892,13 @@ export default function PDFViewer({
|
|||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{/* lg+ = sticky แยกคอลัมน์ (เปิดแล้ว content ขยับไปข้าง ๆ ไม่บังกัน)
|
||||
{/* lg+ = แยกคอลัมน์เต็มความสูง shell (เปิดแล้ว content ขยับไปข้าง ๆ ไม่บังกัน)
|
||||
/ < lg = overlay เลื่อนเข้า-ออก กว้างเป็นสัดส่วนของจอกว้างกว่า 288px
|
||||
(ปิดบน desktop ใช้ lg:hidden ออกจาก flow → main เต็มจอ) */}
|
||||
(ปิดบน desktop ใช้ lg:hidden ออกจาก flow → main เต็มจอ; top-0 จำเป็น
|
||||
ตอน max-lg:fixed ยึดขอบบนจอ) */}
|
||||
<aside
|
||||
data-sidebar=""
|
||||
className={`sticky top-0 flex h-screen w-72 flex-col shrink-0 border-r border-slate-200 bg-white max-lg:fixed max-lg:left-0 max-lg:z-50 max-lg:w-[75vw] max-lg:max-w-72 max-lg:transition-transform max-lg:duration-200 ${
|
||||
className={`top-0 flex h-full w-72 flex-col shrink-0 border-r border-slate-200 bg-white max-lg:fixed max-lg:left-0 max-lg:z-50 max-lg:w-[75vw] max-lg:max-w-72 max-lg:transition-transform max-lg:duration-200 ${
|
||||
sidebarOpen
|
||||
? "max-lg:translate-x-0"
|
||||
: "max-lg:pointer-events-none max-lg:-translate-x-full lg:hidden"
|
||||
|
|
@ -2924,7 +2941,7 @@ export default function PDFViewer({
|
|||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex h-full min-w-0 flex-1 flex-col">
|
||||
{/* แยกเป็นสองแถว: บน = ชื่อไฟล์ + ตัวเลื่อนหน้า/ซูม (กลาง) + หน้าสรุป,
|
||||
ล่าง = แถบ tool + undo/redo + bookmark/export (ชิดขวา) */}
|
||||
<header className="sticky top-0 z-40 border-b border-slate-200 bg-white">
|
||||
|
|
@ -3709,7 +3726,7 @@ export default function PDFViewer({
|
|||
|
||||
<main
|
||||
ref={mainRef}
|
||||
className="space-y-4 px-4 py-4"
|
||||
className="pdf-scroll-main min-h-0 flex-1 space-y-4 overflow-auto overscroll-contain px-4 py-4"
|
||||
style={{
|
||||
cursor:
|
||||
tool === "pan"
|
||||
|
|
@ -3729,7 +3746,7 @@ export default function PDFViewer({
|
|||
onPointerMove={(e) => {
|
||||
const p = panRef.current;
|
||||
if (!p) return;
|
||||
window.scrollBy(p.x - e.clientX, p.y - e.clientY);
|
||||
mainRef.current?.scrollBy(p.x - e.clientX, p.y - e.clientY);
|
||||
panRef.current = { x: e.clientX, y: e.clientY };
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
|
|
@ -3764,7 +3781,7 @@ export default function PDFViewer({
|
|||
= แอปค้าง" (แสดง 3 ครั้งแรกต่อ session) */}
|
||||
{tool === "highlight" && coarseTouch && coarseHlHintVisible && (
|
||||
<p className="mb-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs leading-relaxed text-slate-600">
|
||||
โหมดมาร์กข้อความ: ลากบนหน้าเพื่อคลุมข้อความ ลากชิดขอบบน/ล่างจอเพื่อเลื่อนหน้า
|
||||
โหมดมาร์กข้อความ: ลากบนหน้าเพื่อคลุมข้อความ ลากชิดขอบจอเพื่อเลื่อนหน้า
|
||||
หรือสลับเป็นเครื่องมืออื่นเพื่อปัดเลื่อนตามปกติ
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue