Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 01:56:12 +00:00
co-authored by renee-png
parent 7f3a638ddb
commit 610eb2dcb2
+87 -26
View File
@@ -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: "<p>Start typing here…</p>",
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(`
<p>&nbsp;</p>
<p>______________________________</p>
<p>${label}</p>
`).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("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;").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;
};