@uurtech/jdf 0.1.21 → 0.1.23

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.23",
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";
@@ -181,12 +182,36 @@ function renderImage(el: ImageElement, ctx: RenderContext): HTMLElement {
181
182
 
182
183
  // ── table ───────────────────────────────────────────────────────────────────
183
184
  function cellText(c: TableCellValue): string {
184
- return typeof c === "string" ? c : c.content;
185
+ // Tolerate malformed cells (null, numbers, missing content) instead of
186
+ // throwing — a single bad cell must never abort the whole render. The reader
187
+ // (SolidJS <For>) is naturally lenient here; the web embed's .forEach used to
188
+ // throw on `null.content`, killing the table and every element after it on
189
+ // the page. Coerce anything non-stringy to a best-effort string.
190
+ if (c == null) return "";
191
+ if (typeof c === "string") return c;
192
+ if (typeof c === "object") return c.content == null ? "" : String(c.content);
193
+ return String(c);
185
194
  }
186
195
  function cellAttrs(c: TableCellValue): { colspan?: number; rowspan?: number } {
187
- if (typeof c === "string") return {};
196
+ if (c == null || typeof c !== "object") return {};
188
197
  return { colspan: c.colspan, rowspan: c.rowspan };
189
198
  }
199
+ /** Per-cell style object → CSS map (string ref / array / inline Style). */
200
+ function cellCss(c: TableCellValue, styles: Record<string, Style>): Record<string, string> {
201
+ if (c == null || typeof c !== "object" || !c.style) return {};
202
+ const s = c.style;
203
+ if (typeof s === "string") return styleToCss(styles[s] || {});
204
+ if (Array.isArray(s)) { let m = {}; for (const k of s) m = { ...m, ...styleToCss(styles[k] || {}) }; return m; }
205
+ return styleToCss(s);
206
+ }
207
+ function cellAlign(c: TableCellValue): TextAlign | undefined {
208
+ return c == null || typeof c !== "object" ? undefined : c.align;
209
+ }
210
+ /** Normalise a column width (number → px, string passed through, e.g. "30%"). */
211
+ function colWidthCss(w: string | number | undefined): string | undefined {
212
+ if (w == null) return undefined;
213
+ return typeof w === "number" ? `${unitToPx(w)}px` : w;
214
+ }
190
215
 
191
216
  function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
192
217
  const wrap = document.createElement("div");
@@ -231,9 +256,23 @@ function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
231
256
  const table = document.createElement("table");
232
257
  table.style.width = "100%";
233
258
  table.style.borderCollapse = "collapse";
259
+ table.style.tableLayout = el.columns?.some((c) => c.width != null) ? "fixed" : "auto";
234
260
  table.style.fontSize = "14px";
235
261
  if (borders.outer) table.style.border = `${borders.width || 1}px solid ${borders.color || "#e2e8f0"}`;
236
262
 
263
+ // Column widths — honour columns[].width via a <colgroup> so both header
264
+ // and body cells share the same track sizing.
265
+ if (el.columns?.some((c) => c.width != null)) {
266
+ const colgroup = document.createElement("colgroup");
267
+ el.columns.forEach((c) => {
268
+ const col = document.createElement("col");
269
+ const w = colWidthCss(c.width);
270
+ if (w) col.style.width = w;
271
+ colgroup.appendChild(col);
272
+ });
273
+ table.appendChild(colgroup);
274
+ }
275
+
237
276
  if (headers && headers.length > 0) {
238
277
  const thead = document.createElement("thead");
239
278
  const tr = document.createElement("tr");
@@ -253,7 +292,14 @@ function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
253
292
  }
254
293
 
255
294
  const tbody = document.createElement("tbody");
256
- el.rows.forEach((row, ri) => {
295
+ // Normalise rows/cells defensively: a row that isn't an array (object, null,
296
+ // stray string) or a table whose `rows` isn't an array must not throw and
297
+ // abort the page render. Non-array rows are skipped (like the reader's <For>),
298
+ // matching desktop behaviour so a partly-malformed table still shows its
299
+ // well-formed rows on the web instead of vanishing.
300
+ const rows: TableCellValue[][] = Array.isArray(el.rows) ? el.rows : [];
301
+ rows.forEach((row, ri) => {
302
+ if (!Array.isArray(row)) return;
257
303
  const tr = document.createElement("tr");
258
304
  applyStyle(tr, rowCss as any);
259
305
  if (ri % 2 === 1) applyStyle(tr, altRowCss as any);
@@ -265,8 +311,10 @@ function renderTable(el: TableElement, ctx: RenderContext): HTMLElement {
265
311
  if (attrs.rowspan) td.rowSpan = attrs.rowspan;
266
312
  td.style.padding = "8px 12px";
267
313
  td.style.verticalAlign = "top";
268
- td.style.textAlign = colAlign(ci) || "left";
314
+ // Cell-level align wins over the column default.
315
+ td.style.textAlign = cellAlign(cell) || colAlign(ci) || "left";
269
316
  if (borders.inner) td.style.border = `${borders.width || 1}px solid ${borders.color || "#e2e8f0"}`;
317
+ applyStyle(td, cellCss(cell, ctx.styles));
270
318
  tr.appendChild(td);
271
319
  });
272
320
  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 {