refactor(toolbar): extract toolbar tool logic to module
ย้าย logic ของแถบ tool ออกจาก PDFViewer.tsx ไป pure module ใหม่ components/pdf/toolbar-tools.ts (behavior-preserving) ตามแผน TDD docs/PLAN-TOOLBAR.md ขั้น 1+2 รวม findings #1-#6: - ขั้น 1: ย้าย type Tool/TOOLS/visibleToolsFor/canSwitchTo/ toolButtonFace/menuIntentAfterClick ออกจาก PDFViewer.tsx + rewire + ตัด icon imports ว่าง 9 ตัว + spec test tests/components/toolbar-tools.test.ts - ขั้น 2 (findings #2/#5): TOOL_MENU_KEYS + type MenuTool + TOOL_MENU_TABLE (Record บังคับครบทุก key — เพิ่มเมนูลืมแถว = compile fail) + closeToolMenus()/closePagePopups() ใช้ร่วมครบทุก กลุ่มจุดเดิม รวม 2 จุดที่เดิมไม่เคลียร์เลย (scannedOnly + สลับไฟล์ ภาพ) — popup bookmark/composer/popover ของ pin/แผง selection/editor ข้อความไม่ค้างเปิดซ้อนกันครบทุกทางเข้า - findings #1: คีย์ลัดจับคู่ด้วย e.code แทน e.key (TOOL_SHORTCUTS KeyH/KeyD/KeyC + ขยายครอบ Ctrl+Z undo/redo) — layout ไทยให้ e.key เป็นอักษรไทยจนคีย์ลัด H/D/C/B และ undo ใช้ไม่ได้ - findings #3/#4/#6: ปรับให้ปุ่มใช้ตัวแปร active เดียวกันทั้งชุด (aria-pressed) ผ่าน tsc + vitest 207/207 + e2e Playwright ตามแผน (เมนูค้าง/สลับไฟล์ ภาพ/popup bookmark ลำดับปิด-เปิด/คีย์ลัด synthetic layout ไทย) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
8571ef9aaf
commit
0681e056fd
5 changed files with 561 additions and 148 deletions
108
components/pdf/toolbar-tools.ts
Normal file
108
components/pdf/toolbar-tools.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// pure module ของแถบ tool หน้า annotate — ย้ายจาก PDFViewer.tsx แบบ
|
||||
// behavior-preserving (PLAN-TOOLBAR.md ขั้น 1) ไม่มี React/state เพื่อ unit
|
||||
// test ได้ (tests/components/toolbar-tools.test.ts)
|
||||
import {
|
||||
Eraser,
|
||||
EyeOff,
|
||||
Hand,
|
||||
Highlighter,
|
||||
MessageSquarePlus,
|
||||
MousePointer2,
|
||||
Pencil,
|
||||
Shapes,
|
||||
Stamp,
|
||||
Type,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export type Tool =
|
||||
| "select"
|
||||
| "pan"
|
||||
| "highlight"
|
||||
| "draw"
|
||||
| "comment"
|
||||
| "shape"
|
||||
| "erase"
|
||||
| "redact"
|
||||
| "textbox"
|
||||
| "stamp";
|
||||
|
||||
export type ToolEntry = { key: Tool; label: string; text: string; Icon: LucideIcon };
|
||||
|
||||
/** label = ชื่อเต็มสำหรับ title/aria-label (พร้อมคีย์ลัด/คำอังกฤษประกอบ) —
|
||||
* text = คำสั้น ๆ แสดงเป็นข้อความหลังไอคอนบนปุ่ม (คำขอผู้ใช้ 2026-09-21: ผู้ใช้ศาล
|
||||
* ไม่คุ้นไอคอนล้วน โดยเฉพาะจอสัมผัสที่ hover อ่าน title ไม่ได้ — แทน span ชื่อ
|
||||
* tool-active ของ H3 ที่ถอดออก) — สำหรับปุ่ม dropdown ที่มีตัวเลือกย่อย (มาร์ก
|
||||
* ข้อความ/วาดเส้น/รูปทรง/ตรายาง) เคยแสดงชื่อตัวเลือกล่าสุดแทน text ตลอด (คำขอผู้ใช้
|
||||
* 2026-09-23) แต่ทำให้ชื่อ tool หายไปจากแถบ ("วาดเส้น" เห็นเป็น "ปากกา" ตั้งแต่
|
||||
* ยังไม่ได้เลือกอะไร) — 2026-09-29 จึงแสดงค่าล่าสุด**เฉพาะตอน tool active**
|
||||
* (ค่าที่จะใช้จริงเมื่อวาด) ไม่ active กลับมาแสดง text ชื่อ tool เหมือนปุ่มอื่น */
|
||||
export const TOOLS: ToolEntry[] = [
|
||||
{ key: "select", label: "เลือก", text: "เลือก", Icon: MousePointer2 },
|
||||
{ key: "pan", label: "เลื่อนหน้า (pan)", text: "เลื่อนหน้า", Icon: Hand },
|
||||
{ key: "highlight", label: "มาร์กข้อความ (H)", text: "มาร์กข้อความ", Icon: Highlighter },
|
||||
{ key: "draw", label: "วาดเส้น (D)", text: "วาดเส้น", Icon: Pencil },
|
||||
{ key: "comment", label: "คอมเมนต์ (C)", text: "คอมเมนต์", Icon: MessageSquarePlus },
|
||||
{ key: "shape", label: "รูปทรง", text: "รูปทรง", Icon: Shapes },
|
||||
{ key: "erase", label: "ยางลบ (เส้น/รูปทรง/ตรายางของตัวเอง)", text: "ยางลบ", Icon: Eraser },
|
||||
{ key: "redact", label: "ปิดพราง (redaction)", text: "ปิดพราง", Icon: EyeOff },
|
||||
{ key: "textbox", label: "กล่องข้อความ", text: "กล่องข้อความ", Icon: Type },
|
||||
{ key: "stamp", label: "ตรายาง (stamp)", text: "ตรายาง", Icon: Stamp },
|
||||
];
|
||||
|
||||
/** N3 ไฟล์ภาพ + PDF สแกนล้วน: ไม่มีข้อความให้คลุม — ซ่อน tool มาร์กข้อความ
|
||||
* (HIGHLIGHT) ทั้งปุ่มและเมนูรูปแบบ (hideHighlightTool ของ PDFViewer) */
|
||||
export function visibleToolsFor(hideHighlight: boolean): ToolEntry[] {
|
||||
return hideHighlight ? TOOLS.filter((t) => t.key !== "highlight") : TOOLS;
|
||||
}
|
||||
|
||||
/** ไฟล์ภาพ/ไฟล์สแกนล้วน: มาร์กข้อความใช้ไม่ได้ (ไม่มีข้อความให้คลุม) — ปุ่มถูกซ่อน
|
||||
* ไปแล้ว เหลือทางเข้า tool นี้คือคีย์ลัด H จึงกันที่ switchTool ให้จบ (สลับ tool
|
||||
* อื่นไม่กระทบ) */
|
||||
export function canSwitchTo(t: Tool, hideHighlight: boolean): boolean {
|
||||
return !(t === "highlight" && hideHighlight);
|
||||
}
|
||||
|
||||
/** หน้าตาปุ่ม tool dropdown ที่มีค่าย่อย (มาร์กข้อความ/วาดเส้น/รูปทรง/ตรายาง) —
|
||||
* ไม่ active → text ชื่อ tool + title = label เหมือนปุ่มเดี่ยวอื่น;
|
||||
* active + มีค่า (ค่าที่จะใช้จริงเมื่อวาด เช่น ชื่อปากกา/preset ตรายาง "กำหนดเอง")
|
||||
* → text = ค่า + title = "label — ค่า" ตาม pattern เดิม;
|
||||
* ไม่ส่งค่า (textbox ไม่มีค่าย่อย) → ชื่อ tool แม้ active */
|
||||
export function toolButtonFace(
|
||||
active: boolean,
|
||||
tool: Pick<ToolEntry, "label" | "text">,
|
||||
value?: string | null,
|
||||
): { text: string; title: string } {
|
||||
const showValue = active && value != null;
|
||||
return {
|
||||
text: showValue ? value : tool.text,
|
||||
title: showValue ? `${tool.label} — ${value}` : tool.label,
|
||||
};
|
||||
}
|
||||
|
||||
/** จำ intent เปิด/พับเมนูของปุ่ม dropdown ก่อนเรียก switchTool (ซึ่งปิดเมนูทุกตัว):
|
||||
* กดปุ่มจาก tool อื่น = เปิดเมนูของ tool ที่เพิ่ง active, tool เดิม+เมนูปิด = เปิด,
|
||||
* tool เดิม+เมนูเปิด = พับ (toggle) */
|
||||
export function menuIntentAfterClick(current: Tool, key: Tool, menuOpen: boolean): boolean {
|
||||
return current !== key || !menuOpen;
|
||||
}
|
||||
|
||||
/** tool ที่ปุ่มเป็น dropdown มีเมนูของตัวเอง — ทุกจุดที่ "ปิดเมนูทั้งชุด" (switchTool/
|
||||
* outside-click/resize/auto-select/เปิด popup bookmark/reset tool ตรง) ต้องวนจากชุด
|
||||
* ค่านี้จุดเดียว: แต่ก่อน enumerate setter ด้วยมือ 6 แห่ง จนเมนูใหม่/พฤติกรรมใหม่ลืม
|
||||
* แห่งเดียว = drift เงียบ (เคยเกิดจริง: effect เลื่อนปุ่ม active หาปุ่มไม่เจอเงียบ ๆ
|
||||
* ตั้งแต่ 2026-09-23) — ฝั่ง PDFViewer ผูก setter/ref เป็น Record<MenuTool,…> ให้
|
||||
* ลืมแถว = compile error, ฝั่งนี้ test cross-check กับ TOOLS */
|
||||
export const TOOL_MENU_KEYS = ["highlight", "draw", "shape", "textbox", "stamp"] as const satisfies readonly Tool[];
|
||||
export type MenuTool = (typeof TOOL_MENU_KEYS)[number];
|
||||
|
||||
/** คีย์ลัดสลับ tool (F3) — จับคู่ด้วย `e.code` (ตำแหน่งกายภาพของปุ่มตาม layout
|
||||
* สหรัฐ) ไม่ใช่ `e.key` (ตัวอักษรที่ layout ปัจจุบันจะพิมพ์): ผู้ใช้ศาลส่วนใหญ่สลับ
|
||||
* keyboard เป็นภาษาไทย ซึ่งปุ่มเดียวกันให้ e.key = "ภ" ไม่ใช่ "h" — เช็ค e.key
|
||||
* ทำให้คีย์ลัดทั้งชุดใช้ไม่ได้เลยบน layout ไทย (PLAN-TOOLBAR.md finding #1,
|
||||
* 2026-09-29) */
|
||||
export const TOOL_SHORTCUTS: Readonly<Record<string, Tool>> = {
|
||||
KeyH: "highlight",
|
||||
KeyD: "draw",
|
||||
KeyC: "comment",
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue