structured-render 0.0.5 → 0.0.6
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/augments/shadow-styles.d.ts +6 -1
- package/dist/augments/shadow-styles.js +6 -1
- package/dist/elements/vir-structured-render.element.js +28 -5
- package/dist/render/browser-rendering.js +12 -0
- package/dist/render/render-html.js +25 -30
- package/dist/render/render-pdf.js +68 -42
- package/dist/render/render-types.d.ts +11 -3
- package/dist/render/render-types.js +2 -0
- package/package.json +1 -1
- package/dist/ui/elements/vir-app.element.d.ts +0 -1
- package/dist/ui/elements/vir-app.element.js +0 -9
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Inserts a new stylesheet into a ShadowRoot.
|
|
3
|
+
*
|
|
4
|
+
* @category Internal
|
|
5
|
+
* @returns Whether the new styles were set or not.
|
|
6
|
+
*/
|
|
2
7
|
export declare function insertStyleSheet({ newStyles, oldStyles, shadowRoot, maintainFirstStylesheet, }: {
|
|
3
8
|
newStyles: string;
|
|
4
9
|
oldStyles: string;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { assertWrap, check } from '@augment-vir/assert';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Inserts a new stylesheet into a ShadowRoot.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
* @returns Whether the new styles were set or not.
|
|
7
|
+
*/
|
|
3
8
|
export function insertStyleSheet({ newStyles, oldStyles, shadowRoot, maintainFirstStylesheet, }) {
|
|
4
9
|
if (newStyles !== oldStyles) {
|
|
5
10
|
const sheet = new CSSStyleSheet();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { colorCss } from '@electrovir/color';
|
|
2
2
|
import { css, defineElement, html, listen, unsafeCSS } from 'element-vir';
|
|
3
3
|
import { themeDefaultKey } from 'theme-vir';
|
|
4
|
-
import { noNativeFormStyles, ViraIcon, ViraTag, viraTheme } from 'vira';
|
|
4
|
+
import { noNativeFormStyles, viraFormCssVars, ViraIcon, ViraTag, viraTheme } from 'vira';
|
|
5
5
|
import { insertStyleSheet } from '../augments/shadow-styles.js';
|
|
6
6
|
import { renderStructuredHtml, SourceExpansionEvent } from '../render/render-html.js';
|
|
7
7
|
import { contentDivClass, defaultMarkdownRenderStyles } from '../render/render-markdown-styles.js';
|
|
@@ -34,15 +34,19 @@ export const VirStructuredRender = defineElement()({
|
|
|
34
34
|
styles: ({ cssVars, hostClasses }) => css `
|
|
35
35
|
:host {
|
|
36
36
|
${colorCss(viraTheme.colors[themeDefaultKey])}
|
|
37
|
-
display: flex;
|
|
38
|
-
flex-direction: column;
|
|
39
|
-
align-items: stretch;
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
${ViraIcon} {
|
|
43
40
|
flex-shrink: 0;
|
|
44
41
|
}
|
|
45
42
|
|
|
43
|
+
:host,
|
|
44
|
+
.${unsafeCSS(contentDivClass)}.${unsafeCSS(contentDivClass)}.${unsafeCSS(contentDivClass)} {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
align-items: stretch;
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
.view-header {
|
|
47
51
|
display: flex;
|
|
48
52
|
flex-grow: 1;
|
|
@@ -218,6 +222,7 @@ export const VirStructuredRender = defineElement()({
|
|
|
218
222
|
margin-left: auto;
|
|
219
223
|
width: 32px;
|
|
220
224
|
justify-content: flex-end;
|
|
225
|
+
align-items: center;
|
|
221
226
|
display: flex;
|
|
222
227
|
flex-shrink: 0;
|
|
223
228
|
align-self: top;
|
|
@@ -226,7 +231,25 @@ export const VirStructuredRender = defineElement()({
|
|
|
226
231
|
& .source-icon-button {
|
|
227
232
|
${noNativeFormStyles};
|
|
228
233
|
cursor: pointer;
|
|
229
|
-
color: ${viraTheme.colors['vira-grey-foreground-
|
|
234
|
+
color: ${viraTheme.colors['vira-grey-foreground-header'].foreground.value};
|
|
235
|
+
padding: 2px;
|
|
236
|
+
border-radius: 4px;
|
|
237
|
+
|
|
238
|
+
& ${ViraIcon} {
|
|
239
|
+
display: flex;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
&:hover {
|
|
243
|
+
background-color: ${viraTheme.colors['vira-grey-behind-fg-small-body']
|
|
244
|
+
.background.value};
|
|
245
|
+
color: ${viraFormCssVars['vira-form-accent-primary-color'].value};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
&:active {
|
|
249
|
+
background-color: ${viraTheme.colors['vira-grey-behind-fg-body'].background
|
|
250
|
+
.value};
|
|
251
|
+
color: ${viraFormCssVars['vira-form-accent-primary-color'].value};
|
|
252
|
+
}
|
|
230
253
|
}
|
|
231
254
|
|
|
232
255
|
& ul {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assert, assertWrap } from '@augment-vir/assert';
|
|
2
2
|
import { isRuntimeEnv, mergeDefinedProperties, RuntimeEnv, stringify, } from '@augment-vir/common';
|
|
3
|
+
import { waitForAnimationFrame } from '@augment-vir/web';
|
|
3
4
|
import DOMPurify from 'dompurify';
|
|
4
5
|
import { convertTemplateToString, html } from 'element-vir';
|
|
5
6
|
import { marked } from 'marked';
|
|
@@ -207,6 +208,17 @@ async function renderHtmlToCanvas(htmlString, stylesString, html2PdfOptions) {
|
|
|
207
208
|
* written to the iframe and know the first child is always an element.
|
|
208
209
|
*/
|
|
209
210
|
const contentElement = assertWrap.isDefined(iframeDoc.body.firstElementChild);
|
|
211
|
+
/**
|
|
212
|
+
* Resize the iframe to match the content's natural height so the viewport is not clipped at
|
|
213
|
+
* 0px. Without this, html2canvas may read 0-height dimensions for the element.
|
|
214
|
+
*/
|
|
215
|
+
iframe.style.height = `${iframeDoc.body.scrollHeight}px`;
|
|
216
|
+
/**
|
|
217
|
+
* Wait for the browser to finish layout before html2canvas reads element dimensions and
|
|
218
|
+
* computed styles. Uses the parent window's requestAnimationFrame because the iframe is
|
|
219
|
+
* positioned offscreen, and browsers skip animation frames for invisible iframes.
|
|
220
|
+
*/
|
|
221
|
+
await waitForAnimationFrame(3);
|
|
210
222
|
return await importHtml2Canvas()(contentElement, {
|
|
211
223
|
...html2PdfOptions.html2canvas,
|
|
212
224
|
});
|
|
@@ -2,7 +2,7 @@ import { assert, check } from '@augment-vir/assert';
|
|
|
2
2
|
import { ensureArray, filterMap, mapEnumToObject, mapObjectValues, mergeDefinedProperties, stringify, } from '@augment-vir/common';
|
|
3
3
|
import { extractEventTarget } from '@augment-vir/web';
|
|
4
4
|
import { classMap, css, defineTypedEvent, html, ifDefined, join, listen, nothing, testId, unsafeCSS, } from 'element-vir';
|
|
5
|
-
import { defineTable, ViraCollapsibleWrapper, ViraColorVariant, ViraEmphasis, ViraIcon, ViraSize, ViraTableOrientation, ViraTag, } from 'vira';
|
|
5
|
+
import { defineTable, ViraCollapsibleCard, ViraCollapsibleWrapper, ViraColorVariant, ViraEmphasis, ViraIcon, ViraSize, ViraTableOrientation, ViraTag, } from 'vira';
|
|
6
6
|
import { VirMarkdown } from '../elements/vir-markdown.element.js';
|
|
7
7
|
import { VirSource } from '../elements/vir-source.element.js';
|
|
8
8
|
import { createStructuredRenderIcon } from '../structured-render-data/sections/icon.section.js';
|
|
@@ -338,7 +338,7 @@ const htmlRenderers = {
|
|
|
338
338
|
},
|
|
339
339
|
};
|
|
340
340
|
function renderInternalStructuredHtml(data, options, keyChain) {
|
|
341
|
-
return structuredRenderToHtmlArray(data, options, keyChain).filter(check.isTruthy);
|
|
341
|
+
return structuredRenderToHtmlArray(data, options, keyChain, false).filter(check.isTruthy);
|
|
342
342
|
}
|
|
343
343
|
/**
|
|
344
344
|
* Used for both HTML class names and test ids.
|
|
@@ -357,7 +357,7 @@ export const structuredRenderSectionHtmlNames = mapEnumToObject(StructuredRender
|
|
|
357
357
|
* @category Internal
|
|
358
358
|
*/
|
|
359
359
|
export const structuredRenderSectionHtmlTestId = 'structured-render-section';
|
|
360
|
-
function structuredRenderToHtmlArray(data, options, keyChain) {
|
|
360
|
+
function structuredRenderToHtmlArray(data, options, keyChain, isTopSection) {
|
|
361
361
|
if (!data) {
|
|
362
362
|
return [];
|
|
363
363
|
}
|
|
@@ -365,7 +365,7 @@ function structuredRenderToHtmlArray(data, options, keyChain) {
|
|
|
365
365
|
return data.flatMap((entry, index) => structuredRenderToHtmlArray(entry, options, [
|
|
366
366
|
...keyChain,
|
|
367
367
|
index,
|
|
368
|
-
]));
|
|
368
|
+
], isTopSection));
|
|
369
369
|
}
|
|
370
370
|
else if ('type' in data) {
|
|
371
371
|
const sectionTitle = ('sectionTitle' in data && keyChain.length > 0 && data.sectionTitle) || undefined;
|
|
@@ -375,7 +375,7 @@ function structuredRenderToHtmlArray(data, options, keyChain) {
|
|
|
375
375
|
<div
|
|
376
376
|
class=${classMap({
|
|
377
377
|
'section-wrapper': true,
|
|
378
|
-
'top-section-wrapper':
|
|
378
|
+
'top-section-wrapper': isTopSection,
|
|
379
379
|
[structuredRenderSectionHtmlNames[data.type]]: true,
|
|
380
380
|
})}
|
|
381
381
|
${testId(structuredRenderSectionHtmlTestId)}
|
|
@@ -384,36 +384,31 @@ function structuredRenderToHtmlArray(data, options, keyChain) {
|
|
|
384
384
|
${createSourceWrapper(sectionTemplate, options, keyChain, sources)}
|
|
385
385
|
</div>
|
|
386
386
|
`;
|
|
387
|
-
if (
|
|
388
|
-
const sectionKeyChain = [
|
|
389
|
-
...keyChain,
|
|
390
|
-
'section-collapsible',
|
|
391
|
-
];
|
|
392
|
-
const sectionKey = makeChainKey(sectionKeyChain);
|
|
393
|
-
const isSectionExpanded = !!options.currentlyExpanded[sectionKey];
|
|
387
|
+
if (isTopSection) {
|
|
394
388
|
return [
|
|
395
389
|
html `
|
|
396
|
-
<${
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
expanded: event.detail,
|
|
404
|
-
key: sectionKey,
|
|
405
|
-
}));
|
|
406
|
-
})}
|
|
407
|
-
>
|
|
408
|
-
<h3 slot=${ViraCollapsibleWrapper.slotNames.header}>${sectionTitle}</h3>
|
|
390
|
+
<${ViraCollapsibleCard.assign({
|
|
391
|
+
expandOnPrint: true,
|
|
392
|
+
blockExpansion: options.blockSectionExpansion,
|
|
393
|
+
hideHeader: !sectionTitle,
|
|
394
|
+
startExpanded: options.expandAllSections || keyChain.at(-1) === 0,
|
|
395
|
+
})}>
|
|
396
|
+
<h3 slot=${ViraCollapsibleCard.slotNames.header}>${sectionTitle}</h3>
|
|
409
397
|
${sectionContent}
|
|
410
|
-
</${
|
|
398
|
+
</${ViraCollapsibleCard}>
|
|
411
399
|
`,
|
|
412
400
|
];
|
|
413
401
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
402
|
+
else {
|
|
403
|
+
return [
|
|
404
|
+
sectionTitle
|
|
405
|
+
? html `
|
|
406
|
+
<h3>${sectionTitle}</h3>
|
|
407
|
+
`
|
|
408
|
+
: undefined,
|
|
409
|
+
sectionContent,
|
|
410
|
+
];
|
|
411
|
+
}
|
|
417
412
|
}
|
|
418
413
|
else if ('sections' in data) {
|
|
419
414
|
return [
|
|
@@ -425,7 +420,7 @@ function structuredRenderToHtmlArray(data, options, keyChain) {
|
|
|
425
420
|
...structuredRenderToHtmlArray(data.sections, options, [
|
|
426
421
|
...keyChain,
|
|
427
422
|
'sections',
|
|
428
|
-
]),
|
|
423
|
+
], true),
|
|
429
424
|
];
|
|
430
425
|
}
|
|
431
426
|
else {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isRuntimeEnv, RuntimeEnv } from '@augment-vir/common';
|
|
1
|
+
import { isRuntimeEnv, RuntimeEnv, wait } from '@augment-vir/common';
|
|
2
|
+
import { waitForAnimationFrame } from '@augment-vir/web';
|
|
2
3
|
import { OutputPdfType, renderInBrowser, renderInNode } from './browser-rendering.js';
|
|
3
4
|
/**
|
|
4
5
|
* Render to a PDF. Only works in Node.js, not a browser.
|
|
@@ -50,12 +51,21 @@ export async function printPdf(renderInput, params) {
|
|
|
50
51
|
...params,
|
|
51
52
|
}));
|
|
52
53
|
const blobUrl = URL.createObjectURL(pdfBlob);
|
|
54
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
53
55
|
/**
|
|
54
56
|
* Firefox uses PDF.js to render PDFs in iframes, which doesn't support printing via
|
|
55
|
-
* `contentWindow.print()` on blob URLs.
|
|
56
|
-
*
|
|
57
|
+
* `contentWindow.print()` on blob URLs.
|
|
58
|
+
*
|
|
59
|
+
* IOS Safari renders PDFs in iframes constrained to the iframe's viewport, so a hidden 1x1
|
|
60
|
+
* iframe only prints the first page. iPadOS Safari reports as "Macintosh" in its user agent, so
|
|
61
|
+
* we also check `maxTouchPoints` to detect it.
|
|
62
|
+
*
|
|
63
|
+
* In both cases, open a new window so the platform's native PDF viewer handles all pages.
|
|
57
64
|
*/
|
|
58
|
-
|
|
65
|
+
const isFirefox = userAgent.includes('firefox');
|
|
66
|
+
const isIos = /iphone|ipad|ipod/.test(userAgent) ||
|
|
67
|
+
(userAgent.includes('macintosh') && navigator.maxTouchPoints > 1);
|
|
68
|
+
if (isFirefox || isIos) {
|
|
59
69
|
const printWindow = globalThis.window.open(blobUrl);
|
|
60
70
|
if (!printWindow) {
|
|
61
71
|
URL.revokeObjectURL(blobUrl);
|
|
@@ -73,46 +83,62 @@ export async function printPdf(renderInput, params) {
|
|
|
73
83
|
printFrame.style.opacity = '0';
|
|
74
84
|
printFrame.src = blobUrl;
|
|
75
85
|
globalThis.document.body.append(printFrame);
|
|
86
|
+
await new Promise((resolve) => {
|
|
87
|
+
printFrame.addEventListener('load', () => {
|
|
88
|
+
resolve();
|
|
89
|
+
}, {
|
|
90
|
+
once: true,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
const contentWindow = printFrame.contentWindow;
|
|
94
|
+
if (!contentWindow) {
|
|
95
|
+
URL.revokeObjectURL(blobUrl);
|
|
96
|
+
printFrame.remove();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The iframe's `load` event fires when the blob data is fetched, but the browser's built-in PDF
|
|
101
|
+
* viewer still needs time to parse and paint the PDF. Without this wait, `print()` can capture
|
|
102
|
+
* a blank/grey frame roughly 50% of the time. Animation frames alone are insufficient because
|
|
103
|
+
* the PDF viewer renders asynchronously outside the normal DOM paint cycle, so an additional
|
|
104
|
+
* delay is needed to let it finish.
|
|
105
|
+
*/
|
|
106
|
+
await waitForAnimationFrame(3);
|
|
107
|
+
await wait({
|
|
108
|
+
milliseconds: 250,
|
|
109
|
+
});
|
|
110
|
+
await waitForAnimationFrame(3);
|
|
76
111
|
return new Promise((resolve) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
function cleanup() {
|
|
113
|
+
URL.revokeObjectURL(blobUrl);
|
|
114
|
+
printFrame.remove();
|
|
115
|
+
resolve();
|
|
116
|
+
}
|
|
117
|
+
let resolved = false;
|
|
118
|
+
function resolveOnce() {
|
|
119
|
+
if (resolved) {
|
|
83
120
|
return;
|
|
84
121
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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();
|
|
116
|
-
};
|
|
122
|
+
resolved = true;
|
|
123
|
+
globalThis.window.removeEventListener('focus', focusFallback);
|
|
124
|
+
cleanup();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Fallback: when the print dialog closes (print or cancel), focus returns to the main
|
|
128
|
+
* window. Some browsers don't fire `afterprint` on the iframe's contentWindow when the user
|
|
129
|
+
* cancels.
|
|
130
|
+
*/
|
|
131
|
+
function focusFallback() {
|
|
132
|
+
resolveOnce();
|
|
133
|
+
}
|
|
134
|
+
contentWindow.addEventListener('afterprint', () => {
|
|
135
|
+
resolveOnce();
|
|
136
|
+
}, {
|
|
137
|
+
once: true,
|
|
138
|
+
});
|
|
139
|
+
globalThis.window.addEventListener('focus', focusFallback, {
|
|
140
|
+
once: true,
|
|
141
|
+
});
|
|
142
|
+
contentWindow.print();
|
|
117
143
|
});
|
|
118
144
|
}
|
|
@@ -119,12 +119,20 @@ export type RenderHtmlOptions = RenderOptions & {
|
|
|
119
119
|
* This will only be used if `hideViewOnPageButtons` is not set to `true`.
|
|
120
120
|
*/
|
|
121
121
|
createViewOnPageString(pageNumber: number): string;
|
|
122
|
+
/** CSS styles for rendering internal Markdown. */
|
|
123
|
+
markdownStyles: string | CSSResult;
|
|
122
124
|
/**
|
|
123
|
-
*
|
|
125
|
+
* If set to `true`, all sections will start out expanded.
|
|
124
126
|
*
|
|
125
|
-
* @default
|
|
127
|
+
* @default false
|
|
126
128
|
*/
|
|
127
|
-
|
|
129
|
+
expandAllSections: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* If set to `true`, all sections will be expanded and expansion toggling will be disabled.
|
|
132
|
+
*
|
|
133
|
+
* @default false
|
|
134
|
+
*/
|
|
135
|
+
blockSectionExpansion: boolean;
|
|
128
136
|
};
|
|
129
137
|
/**
|
|
130
138
|
* Default option values for Structured Render rendering to HTML.
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const VirApp: import("element-vir").DeclarativeElementDefinition<"vir-app", {}, {}, {}, "vir-app-", "vir-app-", readonly [], readonly []>;
|