@uurtech/jdf 0.1.18 → 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/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 +34 -1
- 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.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>",
|
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";
|
|
@@ -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
|
-
|
|
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
|
|
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 {
|