diff --git a/src/components/forms/custom-form-builder.tsx b/src/components/forms/custom-form-builder.tsx index 14f90f6..eeedc0b 100644 --- a/src/components/forms/custom-form-builder.tsx +++ b/src/components/forms/custom-form-builder.tsx @@ -395,6 +395,7 @@ export function CustomFormBuilder() { indent: number; caption?: boolean; heading?: 0 | 1 | 2 | 3; + marker?: string; // list marker prefix like "1." or "•" } | { kind: "table"; rows: TableCellSeg[][] }; @@ -472,6 +473,44 @@ export function CustomFormBuilder() { tmp.innerHTML = html; const out: Segment[] = []; + const LIST_INDENT = 24; // px per nesting level + + const pushListItems = ( + listEl: HTMLElement, + ordered: boolean, + depth: number, + ) => { + const items = Array.from(listEl.children).filter( + (c) => (c as HTMLElement).tagName.toLowerCase() === "li", + ) as HTMLElement[]; + let n = 1; + for (const li of items) { + // Collect inline runs from this li, excluding nested lists. + const inlineNodes = Array.from(li.childNodes).filter((c) => { + if (c.nodeType !== Node.ELEMENT_NODE) return true; + const t = (c as HTMLElement).tagName.toLowerCase(); + return t !== "ol" && t !== "ul"; + }); + const runs: InlineRun[] = []; + inlineNodes.forEach((n) => runs.push(...collectRuns(n))); + const merged = mergeRuns(runs); + out.push({ + kind: "text", + runs: merged, + align: "left", + indent: depth * LIST_INDENT, + marker: ordered ? `${n}.` : "•", + }); + n += 1; + // Recurse into any nested lists. + Array.from(li.children).forEach((child) => { + const tag = (child as HTMLElement).tagName.toLowerCase(); + if (tag === "ol") pushListItems(child as HTMLElement, true, depth + 1); + else if (tag === "ul") pushListItems(child as HTMLElement, false, depth + 1); + }); + } + }; + const pushBlock = (el: HTMLElement) => { const tag = el.tagName.toLowerCase(); if (tag === "table") { @@ -489,7 +528,9 @@ export function CustomFormBuilder() { out.push({ kind: "table", rows }); return; } - if (["p", "h1", "h2", "h3", "li"].includes(tag)) { + if (tag === "ol") return pushListItems(el, true, 0); + if (tag === "ul") return pushListItems(el, false, 0); + if (["p", "h1", "h2", "h3"].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"; @@ -504,7 +545,7 @@ export function CustomFormBuilder() { }); return; } - // Recurse into wrappers (lists, etc.) + // Recurse into wrappers we don't recognize. Array.from(el.children).forEach((child) => pushBlock(child as HTMLElement)); }; @@ -717,7 +758,10 @@ export function CustomFormBuilder() { const indentPt = indentPx * 0.75; // px → pt const segMaxW = maxW - indentPt; const startX = margin + indentPt; - const visualLines = runsToLines(seg.runs); + const runsForLayout: InlineRun[] = seg.marker + ? [{ text: `${seg.marker} ` }, ...seg.runs] + : seg.runs; + const visualLines = runsToLines(runsForLayout); if (visualLines.length === 0 || (visualLines.length === 1 && visualLines[0].length === 0)) { y += lineH; } else { @@ -858,12 +902,19 @@ export function CustomFormBuilder() { ? AlignmentType.JUSTIFIED : AlignmentType.LEFT; const indentTwips = Math.round((seg.indent || 0) * 15); // ~px → twips (1px≈15twip) + const baseRuns = buildDocxRuns(seg.runs, { italic: seg.caption }); + const runsWithMarker = seg.marker + ? [ + new TextRun({ text: `${seg.marker}\t`, font: fontFamily, size: fontSize * 2 }), + ...baseRuns, + ] + : baseRuns; children.push( new DocxParagraph({ alignment: align, spacing: docxLineSpacing, - indent: indentTwips ? { left: indentTwips } : undefined, - children: buildDocxRuns(seg.runs, { italic: seg.caption }), + indent: indentTwips ? { left: indentTwips, hanging: seg.marker ? 360 : undefined } : seg.marker ? { left: 360, hanging: 360 } : undefined, + children: runsWithMarker, }), ); }