import type { TextboxSegment } from "@/db/schema"; // แปลงข้อความ/segments ของกล่องข้อความ (TEXTBOX F17) ไป–กลับระหว่าง model ใน DB // กับ DOM ของ contentEditable ใน TextboxEditor (components/pdf/TextboxStampLayer.tsx) // — แยกเป็น pure DOM util (ไม่แตะ React) เพื่อ unit test กับ jsdom ได้ตาม pattern // tests/hooks/useCustomSelection-dom.test.ts (2026-09-28) /** แปลงข้อความ/segments → HTML สำหรับ contentEditable (escape + +
) */ export function segmentsToHtml(text: string, segments?: TextboxSegment[]): string { const esc = (s: string) => s.replace(/&/g, "&").replace(//g, ">"); const runs = segments?.length ? segments : [{ text }]; return runs .map((s) => (s.bold ? `${esc(s.text)}` : esc(s.text))) .join("") .replace(/\n/g, "
"); } /** อ่าน DOM ของ contentEditable กลับเป็น text รวม + segments แยกตามตัวหนา/ปกติ * - ขึ้นบรรทัดใหม่มาจาก 2 แหล่ง:
(Enter ถูกดักเป็น insertLineBreak —
ล้วน) * และ
/

ที่ browser สร้างเอง (paste/โหมดแก้ไขของ browser อื่น) — เจอ * block ใหม่ให้ push "\n" *ก่อน* เข้า block จากตำแหน่งข้อความที่ไปก่อนหน้า: * push หลังจบ block แบบเดิมจะตกขอบทั้งรอยต่อ text→div แรก ("abc

def
" * ของ Chrome กลายเป็น "abcdef") และ block ตัวสุดท้ายของก้อน * - whitespace ท้ายทั้งก้อนถูกตัดออกทั้ง text และ segments ให้ตรงกัน (browser เติม *
ปิดท้ายให้ contentEditable จนได้ "\n" ค้างใน DB) */ export function serializeEditor(root: HTMLElement): { text: string; segments: TextboxSegment[]; } { const segs: TextboxSegment[] = []; const push = (text: string, bold: boolean) => { if (!text) return; const last = segs[segs.length - 1]; if (last && !!last.bold === bold) last.text += text; else segs.push({ text, ...(bold ? { bold: true } : {}) }); }; const walk = (node: Node, bold: boolean) => { if (node.nodeType === Node.TEXT_NODE) { push(node.nodeValue ?? "", bold); } else if (node.nodeName === "BR") { push("\n", bold); } else { const b = bold || node.nodeName === "B" || node.nodeName === "STRONG"; if ( /^(DIV|P)$/.test(node.nodeName) && segs.length > 0 && !segs[segs.length - 1].text.endsWith("\n") ) { push("\n", b); } node.childNodes.forEach((c) => walk(c, b)); } }; walk(root, false); const joined = segs.map((s) => s.text).join(""); let cut = joined.length - joined.trimEnd().length; for (let i = segs.length - 1; i >= 0 && cut > 0; i--) { if (segs[i].text.length <= cut) { cut -= segs[i].text.length; segs.splice(i, 1); } else { segs[i].text = segs[i].text.slice(0, segs[i].text.length - cut); cut = 0; } } return { text: joined.trimEnd(), segments: segs }; }