From 610eb2dcb2a04fc7ca30833153760fb0b6116f54 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 01:56:12 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/forms/custom-form-builder.tsx | 113 ++++++++++++++----- 1 file changed, 87 insertions(+), 26 deletions(-) diff --git a/src/components/forms/custom-form-builder.tsx b/src/components/forms/custom-form-builder.tsx index 3e6dbad..1668927 100644 --- a/src/components/forms/custom-form-builder.tsx +++ b/src/components/forms/custom-form-builder.tsx @@ -170,14 +170,20 @@ export function CustomFormBuilder() { const editor = useEditor({ extensions: [ - StarterKit.configure({ heading: { levels: [1, 2, 3] } }), + StarterKit.configure({ heading: { levels: [1, 2, 3] }, paragraph: false }), + PaddedParagraph, Underline, TextAlign.configure({ types: ["heading", "paragraph"] }), + Table.configure({ resizable: true, HTMLAttributes: { class: "cf-table" } }), + TableRow, + TableHeader, + TableCell, ], content: "

Start typing here…

", editorProps: { attributes: { - class: "prose prose-sm max-w-none focus:outline-none px-10 py-8 bg-white text-black min-h-[500px]", + class: + "prose prose-sm max-w-none focus:outline-none px-10 py-8 bg-white text-black min-h-[500px]", }, }, }); @@ -212,46 +218,101 @@ export function CustomFormBuilder() { const insertSignatureLine = (label: string = "Signature") => { if (!editor) return; - editor.chain().focus().insertContent(` -

 

-

______________________________

-

${label}

- `).run(); + editor + .chain() + .focus() + .insertContent([ + { type: "paragraph", content: [{ type: "text", text: " " }] }, + { type: "paragraph", content: [{ type: "text", text: "______________________________" }] }, + { type: "paragraph", content: [{ type: "text", text: label }] }, + ]) + .run(); }; + const INDENT_STEP = 36; // ~0.5" + const indent = () => { if (!editor) return; - // tiptap doesn't have indent built-in; emulate with non-breaking spaces at start - editor.chain().focus().insertContent("     ").run(); + const cur = Number(editor.getAttributes("paragraph").indent ?? 0); + editor.chain().focus().updateAttributes("paragraph", { indent: cur + INDENT_STEP }).run(); }; const outdent = () => { if (!editor) return; - // Best-effort: not perfectly removing — let user use undo. We expose for symmetry. - editor.chain().focus().run(); + const cur = Number(editor.getAttributes("paragraph").indent ?? 0); + editor + .chain() + .focus() + .updateAttributes("paragraph", { indent: Math.max(0, cur - INDENT_STEP) }) + .run(); }; - // Convert editor HTML into plain text lines for PDF/docx (preserves paragraphs). - const getRenderedSegments = (): { text: string; align: "left" | "center" | "right" | "justify"; bold?: boolean; italic?: boolean }[] => { + const insertTable = () => { + if (!editor) return; + editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); + }; + + const toggleCaption = () => { + if (!editor) return; + const cur = Boolean(editor.getAttributes("paragraph").caption); + editor.chain().focus().updateAttributes("paragraph", { caption: !cur }).run(); + }; + + // Linearized document segments for export. Tables are flattened into a "table" segment + // containing 2D rows of strings; paragraphs become text segments preserving alignment + indent. + type Segment = + | { + kind: "text"; + text: string; + align: "left" | "center" | "right" | "justify"; + indent: number; + caption?: boolean; + heading?: 0 | 1 | 2 | 3; + } + | { kind: "table"; rows: string[][]; hasHeader: boolean }; + + const getRenderedSegments = (): Segment[] => { if (!editor) return []; const html = editor.getHTML(); const tmp = document.createElement("div"); tmp.innerHTML = html; - const out: { text: string; align: "left" | "center" | "right" | "justify"; bold?: boolean; italic?: boolean }[] = []; - const walk = (node: ChildNode) => { - if (node.nodeType === Node.ELEMENT_NODE) { - const el = node as HTMLElement; - const tag = el.tagName.toLowerCase(); - if (["p", "h1", "h2", "h3", "li"].includes(tag)) { - const align = (el.style.textAlign as any) || "left"; - const text = el.textContent || ""; - out.push({ text, align }); - return; - } + const out: Segment[] = []; + + const cellText = (cell: HTMLElement) => (cell.textContent ?? "").replace(/\s+/g, " ").trim(); + + const pushBlock = (el: HTMLElement) => { + const tag = el.tagName.toLowerCase(); + if (tag === "table") { + const rows: string[][] = []; + let hasHeader = false; + el.querySelectorAll("tr").forEach((tr) => { + const cells = Array.from(tr.children) as HTMLElement[]; + if (cells.some((c) => c.tagName.toLowerCase() === "th")) hasHeader = true; + rows.push(cells.map(cellText)); + }); + out.push({ kind: "table", rows, hasHeader }); + return; } - node.childNodes.forEach(walk); + if (["p", "h1", "h2", "h3", "li"].includes(tag)) { + const align = (el.style.textAlign as any) || "left"; + const indentPx = parseInt(el.style.paddingLeft || "0", 10) || 0; + const caption = el.getAttribute("data-caption") === "true"; + const heading = tag === "h1" ? 1 : tag === "h2" ? 2 : tag === "h3" ? 3 : 0; + out.push({ + kind: "text", + text: el.textContent || "", + align, + indent: indentPx, + caption, + heading: heading as 0 | 1 | 2 | 3, + }); + return; + } + // Recurse into wrappers (lists, etc.) + Array.from(el.children).forEach((child) => pushBlock(child as HTMLElement)); }; - tmp.childNodes.forEach(walk); + + Array.from(tmp.children).forEach((el) => pushBlock(el as HTMLElement)); return out; };