structured-render 0.0.3 → 0.0.5

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.
@@ -10,8 +10,24 @@ import { type RenderHtmlOptions, type RenderInput } from '../render/render-types
10
10
  export declare const VirStructuredRender: import("element-vir").DeclarativeElementDefinition<"vir-structured-render", {
11
11
  data: Readonly<RenderInput>;
12
12
  options?: Readonly<PartialWithUndefined<RenderHtmlOptions & {
13
- isTableSize: boolean;
13
+ /**
14
+ * If `true`, smaller, tablet-compatible styles are used.
15
+ *
16
+ * @default false
17
+ */
18
+ isTabletSize: boolean;
19
+ /**
20
+ * If `true`, smaller, phone-compatible styles are used.
21
+ *
22
+ * @default false
23
+ */
14
24
  isPhoneSize: boolean;
25
+ /**
26
+ * If `true`, all sections will automatically be expanded at first.
27
+ *
28
+ * @default false
29
+ */
30
+ expandAllSections: boolean;
15
31
  }>> | undefined;
16
32
  }, {
17
33
  currentlyExpanded: Record<string, boolean>;
@@ -29,7 +29,7 @@ export const VirStructuredRender = defineElement()({
29
29
  },
30
30
  hostClasses: {
31
31
  'vir-structured-render-phone-size': ({ inputs }) => !!inputs.options?.isPhoneSize,
32
- 'vir-structured-render-tablet-size': ({ inputs }) => !!inputs.options?.isTableSize,
32
+ 'vir-structured-render-tablet-size': ({ inputs }) => !!inputs.options?.isTabletSize,
33
33
  },
34
34
  styles: ({ cssVars, hostClasses }) => css `
35
35
  :host {
@@ -132,7 +132,17 @@ export async function renderInBrowser(structuredRenderData, { fileName, outputTy
132
132
  const instance = htmlToPdf().set(html2PdfOptions).from(canvas);
133
133
  if (outputType.pdf) {
134
134
  if (outputType.pdf === OutputPdfType.Download) {
135
- return await instance.save(fileName);
135
+ const pdfBlob = (await instance.outputPdf(OutputPdfType.Blob));
136
+ const blobUrl = URL.createObjectURL(pdfBlob);
137
+ const anchor = globalThis.document.createElement('a');
138
+ anchor.href = blobUrl;
139
+ anchor.download = fileName.endsWith('.pdf') ? fileName : `${fileName}.pdf`;
140
+ anchor.style.display = 'none';
141
+ globalThis.document.body.append(anchor);
142
+ anchor.click();
143
+ anchor.remove();
144
+ URL.revokeObjectURL(blobUrl);
145
+ return;
136
146
  }
137
147
  else {
138
148
  return await instance.outputPdf(outputType.pdf, {
@@ -371,25 +371,48 @@ function structuredRenderToHtmlArray(data, options, keyChain) {
371
371
  const sectionTitle = ('sectionTitle' in data && keyChain.length > 0 && data.sectionTitle) || undefined;
372
372
  const sectionTemplate = htmlRenderers[data.type](data, options, keyChain);
373
373
  const sources = ('sources' in data && data.sources) || undefined;
374
+ const sectionContent = html `
375
+ <div
376
+ class=${classMap({
377
+ 'section-wrapper': true,
378
+ 'top-section-wrapper': keyChain.length === 0,
379
+ [structuredRenderSectionHtmlNames[data.type]]: true,
380
+ })}
381
+ ${testId(structuredRenderSectionHtmlTestId)}
382
+ ${testId(structuredRenderSectionHtmlNames[data.type])}
383
+ >
384
+ ${createSourceWrapper(sectionTemplate, options, keyChain, sources)}
385
+ </div>
386
+ `;
387
+ if (sectionTitle) {
388
+ const sectionKeyChain = [
389
+ ...keyChain,
390
+ 'section-collapsible',
391
+ ];
392
+ const sectionKey = makeChainKey(sectionKeyChain);
393
+ const isSectionExpanded = !!options.currentlyExpanded[sectionKey];
394
+ return [
395
+ html `
396
+ <${ViraCollapsibleWrapper.assign({
397
+ expanded: isSectionExpanded,
398
+ })}
399
+ class="section-collapsible-wrapper"
400
+ ${listen(ViraCollapsibleWrapper.events.expandChange, (event) => {
401
+ const eventTarget = extractEventTarget(event, HTMLElement);
402
+ eventTarget.dispatchEvent(new SourceExpansionEvent({
403
+ expanded: event.detail,
404
+ key: sectionKey,
405
+ }));
406
+ })}
407
+ >
408
+ <h3 slot=${ViraCollapsibleWrapper.slotNames.header}>${sectionTitle}</h3>
409
+ ${sectionContent}
410
+ </${ViraCollapsibleWrapper}>
411
+ `,
412
+ ];
413
+ }
374
414
  return [
375
- sectionTitle
376
- ? html `
377
- <h3>${sectionTitle}</h3>
378
- `
379
- : undefined,
380
- html `
381
- <div
382
- class=${classMap({
383
- 'section-wrapper': true,
384
- 'top-section-wrapper': keyChain.length === 0,
385
- [structuredRenderSectionHtmlNames[data.type]]: true,
386
- })}
387
- ${testId(structuredRenderSectionHtmlTestId)}
388
- ${testId(structuredRenderSectionHtmlNames[data.type])}
389
- >
390
- ${createSourceWrapper(sectionTemplate, options, keyChain, sources)}
391
- </div>
392
- `,
415
+ sectionContent,
393
416
  ];
394
417
  }
395
418
  else if ('sections' in data) {
@@ -50,16 +50,69 @@ export async function printPdf(renderInput, params) {
50
50
  ...params,
51
51
  }));
52
52
  const blobUrl = URL.createObjectURL(pdfBlob);
53
+ /**
54
+ * Firefox uses PDF.js to render PDFs in iframes, which doesn't support printing via
55
+ * `contentWindow.print()` on blob URLs. Open a new window instead so Firefox's native PDF
56
+ * viewer handles it.
57
+ */
58
+ if (navigator.userAgent.toLowerCase().includes('firefox')) {
59
+ const printWindow = globalThis.window.open(blobUrl);
60
+ if (!printWindow) {
61
+ URL.revokeObjectURL(blobUrl);
62
+ throw new Error('Failed to open print window. Check your popup blocker settings.');
63
+ }
64
+ return;
65
+ }
53
66
  const printFrame = globalThis.document.createElement('iframe');
54
- printFrame.style.display = 'none';
67
+ printFrame.style.position = 'fixed';
68
+ printFrame.style.left = '-10000px';
69
+ printFrame.style.top = '0';
70
+ printFrame.style.width = '1px';
71
+ printFrame.style.height = '1px';
72
+ printFrame.style.border = 'none';
73
+ printFrame.style.opacity = '0';
55
74
  printFrame.src = blobUrl;
56
75
  globalThis.document.body.append(printFrame);
57
76
  return new Promise((resolve) => {
58
77
  printFrame.onload = () => {
59
- printFrame.contentWindow?.print();
60
- URL.revokeObjectURL(blobUrl);
61
- resolve();
62
- printFrame.remove();
78
+ const contentWindow = printFrame.contentWindow;
79
+ if (!contentWindow) {
80
+ URL.revokeObjectURL(blobUrl);
81
+ printFrame.remove();
82
+ resolve();
83
+ return;
84
+ }
85
+ function cleanup() {
86
+ URL.revokeObjectURL(blobUrl);
87
+ printFrame.remove();
88
+ resolve();
89
+ }
90
+ let resolved = false;
91
+ function resolveOnce() {
92
+ if (resolved) {
93
+ return;
94
+ }
95
+ resolved = true;
96
+ globalThis.window.removeEventListener('focus', focusFallback);
97
+ cleanup();
98
+ }
99
+ /**
100
+ * Fallback: when the print dialog closes (print or cancel), focus returns to the main
101
+ * window. Some browsers don't fire `afterprint` on the iframe's contentWindow when the
102
+ * user cancels.
103
+ */
104
+ function focusFallback() {
105
+ resolveOnce();
106
+ }
107
+ contentWindow.addEventListener('afterprint', () => {
108
+ resolveOnce();
109
+ }, {
110
+ once: true,
111
+ });
112
+ globalThis.window.addEventListener('focus', focusFallback, {
113
+ once: true,
114
+ });
115
+ contentWindow.print();
63
116
  };
64
117
  });
65
118
  }
@@ -0,0 +1 @@
1
+ export declare const VirApp: import("element-vir").DeclarativeElementDefinition<"vir-app", {}, {}, {}, "vir-app-", "vir-app-", readonly [], readonly []>;
@@ -0,0 +1,9 @@
1
+ import { defineElement, html } from 'element-vir';
2
+ export const VirApp = defineElement()({
3
+ tagName: 'vir-app',
4
+ render() {
5
+ return html `
6
+ Vir App goes here!
7
+ `;
8
+ },
9
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "structured-render",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A library for safely rendering arbitrary data generated from any source.",
5
5
  "keywords": [
6
6
  "render",
@@ -52,7 +52,7 @@
52
52
  "object-shape-tester": "^6.11.0",
53
53
  "theme-vir": "^28.22.0",
54
54
  "type-fest": "^5.4.4",
55
- "vira": "^29.7.2"
55
+ "vira": "^29.8.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@augment-vir/test": "^31.67.0",