@uurtech/jdf 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uurtech/jdf",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Render JDF (JSON Document Format) documents in the browser. Drop-in replacement for embedding PDFs — point at a .jdf file and it appears on the page, fully styled, searchable, with a clickable TOC.",
5
5
  "license": "MIT",
6
6
  "author": "Ugur Kazdal <ugur@uurtech.com>",
@@ -3,6 +3,7 @@ import type {
3
3
  TextElement, RichTextElement, ImageElement, TableElement, ListElement,
4
4
  ShapeElement, CollapsibleElement, TocElement, RichTextRun, ListItem, TableCellValue, ImageResource,
5
5
  FormInputElement, FormTextareaElement, FormCheckboxElement, FormSelectElement, FormSignatureElement,
6
+ TextAlign,
6
7
  } from "@jdf/core";
7
8
  import { unitToPx } from "@jdf/core";
8
9
  import { resolveStyle, styleToCss, applyStyle } from "../utils/style";
@@ -187,6 +188,22 @@ function cellAttrs(c: TableCellValue): { colspan?: number; rowspan?: number } {
187
188
  if (typeof c === "string") return {};
188
189
  return { colspan: c.colspan, rowspan: c.rowspan };
189
190
  }
191
+ /** Per-cell style object → CSS map (string ref / array / inline Style). */
192
+ function cellCss(c: TableCellValue, styles: Record<string, Style>): Record<string, string> {
193
+ if (typeof c === "string" || !c.style) return {};
194
+ const s = c.style;
195
+ if (typeof s === "string") return styleToCss(styles[s] || {});
196
+ if (Array.isArray(s)) { let m = {}; for (const k of s) m = { ...m, ...styleToCss(styles[k] || {}) }; return m; }
197
+ return styleToCss(s);
198
+ }
199
+ function cellAlign(c: TableCellValue): TextAlign | undefined {
200
+ return typeof c === "string" ? undefined : c.align;
201
+ }
202
+ /** Normalise a column width (number → px, string passed through, e.g. "30%"). */
203
+ function colWidthCss(w: string | number | undefined): string | undefined {
204
+ if (w == null) return undefined;
205
+ return typeof w === "number" ? `${unitToPx(w)}px` : w;
206
+ }
190
207
 
191
208
  function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
192
209
  const wrap = document.createElement("div");
@@ -231,9 +248,23 @@ function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
231
248
  const table = document.createElement("table");
232
249
  table.style.width = "100%";
233
250
  table.style.borderCollapse = "collapse";
251
+ table.style.tableLayout = el.columns?.some((c) => c.width != null) ? "fixed" : "auto";
234
252
  table.style.fontSize = "14px";
235
253
  if (borders.outer) table.style.border = `${borders.width || 1}px solid ${borders.color || "#e2e8f0"}`;
236
254
 
255
+ // Column widths — honour columns[].width via a <colgroup> so both header
256
+ // and body cells share the same track sizing.
257
+ if (el.columns?.some((c) => c.width != null)) {
258
+ const colgroup = document.createElement("colgroup");
259
+ el.columns.forEach((c) => {
260
+ const col = document.createElement("col");
261
+ const w = colWidthCss(c.width);
262
+ if (w) col.style.width = w;
263
+ colgroup.appendChild(col);
264
+ });
265
+ table.appendChild(colgroup);
266
+ }
267
+
237
268
  if (headers && headers.length > 0) {
238
269
  const thead = document.createElement("thead");
239
270
  const tr = document.createElement("tr");
@@ -265,8 +296,10 @@ function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
265
296
  if (attrs.rowspan) td.rowSpan = attrs.rowspan;
266
297
  td.style.padding = "8px 12px";
267
298
  td.style.verticalAlign = "top";
268
- td.style.textAlign = colAlign(ci) || "left";
299
+ // Cell-level align wins over the column default.
300
+ td.style.textAlign = cellAlign(cell) || colAlign(ci) || "left";
269
301
  if (borders.inner) td.style.border = `${borders.width || 1}px solid ${borders.color || "#e2e8f0"}`;
302
+ applyStyle(td, cellCss(cell, ctx.styles));
270
303
  tr.appendChild(td);
271
304
  });
272
305
  tbody.appendChild(tr);
package/src/viewer.ts CHANGED
@@ -403,7 +403,8 @@ export class JDFViewer {
403
403
  const footerH = footer?.height ?? 0;
404
404
 
405
405
  if (header) {
406
- const h = this.renderHeaderFooter(header, pageIndex, this.doc.pages.length, styles);
406
+ const headerPath: (string | number)[] = page.header ? ["pages", pageIndex, "header"] : ["header"];
407
+ const h = this.renderHeaderFooter(header, pageIndex, this.doc.pages.length, styles, headerPath);
407
408
  h.classList.add("jdfjs-header");
408
409
  h.style.paddingTop = `${unitToPx(margins.top! / 2)}px`;
409
410
  h.style.paddingLeft = `${unitToPx(margins.left!)}px`;
@@ -438,7 +439,8 @@ export class JDFViewer {
438
439
  pageEl.appendChild(content);
439
440
 
440
441
  if (footer) {
441
- const f = this.renderHeaderFooter(footer, pageIndex, this.doc.pages.length, styles);
442
+ const footerPath: (string | number)[] = page.footer ? ["pages", pageIndex, "footer"] : ["footer"];
443
+ const f = this.renderHeaderFooter(footer, pageIndex, this.doc.pages.length, styles, footerPath);
442
444
  f.classList.add("jdfjs-footer");
443
445
  f.style.paddingBottom = `${unitToPx(margins.bottom! / 2)}px`;
444
446
  f.style.paddingLeft = `${unitToPx(margins.left!)}px`;
@@ -450,7 +452,7 @@ export class JDFViewer {
450
452
  return wrapper;
451
453
  }
452
454
 
453
- private renderHeaderFooter(hf: HeaderFooter, pageIndex: number, totalPages: number, styles: Record<string, Style>): HTMLDivElement {
455
+ private renderHeaderFooter(hf: HeaderFooter, pageIndex: number, totalPages: number, styles: Record<string, Style>, basePath: (string | number)[]): HTMLDivElement {
454
456
  const div = document.createElement("div");
455
457
  if (hf.elements?.length) {
456
458
  hf.elements.forEach((el, idx) => {
@@ -458,8 +460,12 @@ export class JDFViewer {
458
460
  styles,
459
461
  resources: this.doc.resources,
460
462
  document: this.doc,
461
- path: ["__hf__", pageIndex, idx],
463
+ // Real doc path so form-field mutations land on the right node and
464
+ // survive exportJdf (was a synthetic ["__hf__", …] that
465
+ // handleFormChange could not resolve).
466
+ path: [...basePath, "elements", idx],
462
467
  onNavigatePage: (i) => this.goToPage(i),
468
+ onFormChange: (path, field, value) => this.handleFormChange(path, field, value),
463
469
  });
464
470
  if (node) div.appendChild(node);
465
471
  });
@@ -559,18 +565,40 @@ export class JDFViewer {
559
565
  }
560
566
 
561
567
  /**
562
- * Walk every page's elements and yield each form element with its
563
- * resolved name. Used by getFormValues / downloadJdf consumers.
568
+ * Walk every page's elements — recursing into container elements
569
+ * (collapsible / table cells / list items) and the doc + per-page
570
+ * header/footer trees — and yield each form element with its resolved
571
+ * name. Previously this only scanned top-level `page.elements`, so a
572
+ * field nested inside a collapsible section or placed in a header was
573
+ * silently dropped from getFormValues / downloadJdf.
564
574
  */
565
575
  private *iterFormFields(): Generator<{ name: string; field: any }> {
566
- for (const page of this.doc.pages || []) {
567
- for (const el of (page.elements || []) as any[]) {
568
- if (!el || typeof el !== "object") continue;
569
- if (el.type === "input" || el.type === "textarea" || el.type === "checkbox" || el.type === "select" || el.type === "signature") {
570
- if (typeof el.name === "string" && el.name.length > 0) yield { name: el.name, field: el };
571
- }
576
+ const seen = new Set<any>();
577
+ const walk = function* (node: any): Generator<{ name: string; field: any }> {
578
+ if (!node || typeof node !== "object" || seen.has(node)) return;
579
+ seen.add(node);
580
+ if (Array.isArray(node)) {
581
+ for (const item of node) yield* walk(item);
582
+ return;
583
+ }
584
+ const t = node.type;
585
+ if (t === "input" || t === "textarea" || t === "checkbox" || t === "select" || t === "signature") {
586
+ if (typeof node.name === "string" && node.name.length > 0) yield { name: node.name, field: node };
572
587
  }
588
+ // Recurse into any nested element carriers.
589
+ if (Array.isArray(node.elements)) yield* walk(node.elements);
590
+ if (Array.isArray(node.items)) yield* walk(node.items);
591
+ if (Array.isArray(node.rows)) {
592
+ for (const row of node.rows) if (Array.isArray(row)) yield* walk(row);
593
+ }
594
+ };
595
+ for (const page of this.doc.pages || []) {
596
+ yield* walk(page.elements);
597
+ yield* walk(page.header?.elements);
598
+ yield* walk(page.footer?.elements);
573
599
  }
600
+ yield* walk(this.doc.header?.elements);
601
+ yield* walk(this.doc.footer?.elements);
574
602
  }
575
603
 
576
604
  toJSON(options: { pretty?: boolean } = {}): string {