@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/dist/jdfjs.cjs +3 -3
- package/dist/jdfjs.cjs.map +1 -1
- package/dist/jdfjs.d.cts +6 -2
- package/dist/jdfjs.d.ts +6 -2
- package/dist/jdfjs.js +3 -3
- package/dist/jdfjs.js.map +1 -1
- package/package.json +1 -1
- package/src/renderers/element.ts +52 -4
- package/src/viewer.ts +40 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uurtech/jdf",
|
|
3
|
-
"version": "0.1.
|
|
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>",
|
package/src/renderers/element.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
563
|
-
*
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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 {
|