eCourt-note/components/pdf/toolbar-tools.ts
Your Name 0681e056fd 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>
2026-09-29 14:47:12 +07:00

108 lines
7.7 KiB
TypeScript

// 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",
};