From e464ecd22bfb733d7bf4132b81d507a3a88158f3 Mon Sep 17 00:00:00 2001 From: danvim Date: Fri, 24 Nov 2023 02:28:22 +0800 Subject: [PATCH] Add PdfDocument, PdfHeader, PdfFooter. Render to TDocumentDefinitions instead of Content. --- README.md | 99 ++++++++++++---- package.json | 2 +- pnpm-lock.yaml | 110 +++++++++--------- screenshots/react-devtools-demo.png | Bin 0 -> 60914 bytes src/PdfPreview.tsx | 118 ++++++++++++------- src/PdfRenderer.ts | 50 --------- src/PdfRenderer.tsx | 71 ++++++++++++ src/ReactPdfMake.ts | 11 ++ src/__tests__/PdfRenderer.test.tsx | 143 +++++++++++++++++++----- src/components/PdfDocument.tsx | 20 ++++ src/components/PdfFooter.tsx | 7 ++ src/components/PdfHeader.tsx | 7 ++ src/components/PdfProvider.ts | 24 ++++ src/components/index.ts | 3 + src/components/withPdfMarginContent.tsx | 63 +++++++++++ src/createContainer.ts | 10 ++ src/demo/App.tsx | 104 ++++++++++------- src/hostConfig.ts | 11 +- src/index.ts | 3 +- src/types/Container.ts | 8 +- src/types/ContentUpdateHandler.ts | 3 - src/types/DocumentUpdateHandler.ts | 3 + src/types/DynamicPdfNode.tsx | 4 + 23 files changed, 617 insertions(+), 257 deletions(-) create mode 100644 screenshots/react-devtools-demo.png delete mode 100644 src/PdfRenderer.ts create mode 100644 src/PdfRenderer.tsx create mode 100644 src/ReactPdfMake.ts create mode 100644 src/components/PdfDocument.tsx create mode 100644 src/components/PdfFooter.tsx create mode 100644 src/components/PdfHeader.tsx create mode 100644 src/components/PdfProvider.ts create mode 100644 src/components/withPdfMarginContent.tsx create mode 100644 src/createContainer.ts delete mode 100644 src/types/ContentUpdateHandler.ts create mode 100644 src/types/DocumentUpdateHandler.ts create mode 100644 src/types/DynamicPdfNode.tsx diff --git a/README.md b/README.md index 9360de3..abd8111 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,16 @@ npm i react-pdfmake-reconciler - Write complex PDF in JSX. Render JSX into PDF Make content structure. - Utilize React features like: - - Context. Note that outside React context does not penetrate into PDF renderer. + - Context. Note that outside React contexts do not penetrate into PDF renderer. - Components - Hooks -- Working React update loop, (although it is unlikely to trigger user events inside PDF.) - - e.g. async setState calls -- Code autocomplete in JSX for "PDF Make components" +- Working React update loop, (although it is unlikely to trigger user events inside PDF.), e.g. + - async setState calls + - useEffect call +- TypeScript typing for PDF Make Components (`` components) +- React Developer Tools support + +![React Developer Tools Demo](./screenshots/react-devtools-demo.png) ## Running demo @@ -32,56 +36,103 @@ pnpm dev ## Usage -See `/demo` and tests for more extensive examples. +See `/demo` and [tests](./src/__tests__/PdfRenderer.test.tsx) for more extensive examples. ### Simple examples ```tsx /// -import { PdfRenderer } from 'react-pdfmake-reconciler/PdfRenderer' +import { PdfRenderer } from "react-pdfmake-reconciler/PdfRenderer"; -const {unmount} = PdfRenderer.render( +const { unmount } = PdfRenderer.render( Hello World!, - content => console.log(content) -) + (document) => console.log(document), +); /* Console: { - $__reactPdfMakeType: 'pdf-text', - text: 'Hello World!', - bold: true + content: { + $__reactPdfMakeType: 'pdf-text', + text: 'Hello World!', + bold: true + } } */ // Call unmount to detach node tree. -unmount() +unmount(); ``` ```tsx -import { PdfRenderer } from 'react-pdfmake-reconciler/PdfRenderer' +import { PdfRenderer } from "react-pdfmake-reconciler/PdfRenderer"; -const content = await PdfRenderer.renderOnce(Hello World!) +const document = PdfRenderer.renderOnce(Hello World!); ``` ### PDF elements -Newly defined intrinsic elements have the `pdf-` prefix. Roughly, each type of PDF Make node corresponds to one element type, where the property specifying `Content` is mapped to the `children` prop. For example: +Newly defined intrinsic elements by this package have the `pdf-` prefix. Roughly speaking, each type of PDF Make content object corresponds to one element type, where the property specifying the `Content` is mapped to the `children` prop. For example: ```tsx const pdfMakeContent = { - text: 'GitHub', - link: 'https://www.github.com' -} + text: "GitHub", + link: "https://www.github.com", +}; // is mapped to +const pdfNode = GitHub; +``` + +There are also virtual element types. For more information, read [JSDocs in types](./src/types/PdfElements.ts) for more information. + +### Document, Header, and Footer + +You can easily define extra document definition props straight inside your JSX using ``. It is optional to put the body of the document inside this component. + +Implemented using React Portals, you can define static/dynamic header and footer using `` and ``. + +These components can appear anywhere within your JSX structure, although you may follow this convention for a better looking structure: + +```tsx +import { PdfDocument, PdfHeader, PdfFooter } from "react-pdfmake-reconciler"; + const pdfNode = ( - - GitHub - -) + + {/* Example static header */} + This is a header + {/* Example dynmaic footer */} + + {(pageNumber, pageCount) => ( + + Page {pageNumber} / {pageCount} + + )} + + {bodyGoesHere} + +); ``` -There are also virtual element types. For more information, read JSDocs in types. +### PdfPreview + +`` provides an easy way to render your React PDF Make Reconciler JSX in the browser. You can also debug your PDF JSX using the [React Developer Tools](https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) browser extension. + +```tsx +import { FC, StrictMode } from "react"; +import { PdfPreview } from "react-pdfmake-reconciler"; + +const App: FC = () => ( +
+ + {/* Optional */} + + {/* Only use components that resolves to pdf-* components from here on out. DOM elements won't work. */} + Hello World! + + +
+); +``` diff --git a/package.json b/package.json index de33fec..552dbf3 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "prettier": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "typescript": "^5.2.2", + "typescript": "^5.3.2", "vite": "^5.0.0", "vite-plugin-dts": "^3.6.3", "vitest": "^0.34.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f89a3c..1535267 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,15 +5,15 @@ settings: excludeLinksFromLockfile: false dependencies: + '@types/pdfkit': + specifier: ^0.13.2 + version: 0.13.2 + '@types/pdfmake': + specifier: ^0.2.8 + version: 0.2.8 pdfmake: specifier: ^0.2.8 version: 0.2.8 - react: - specifier: ^18.2.0 - version: 18.2.0 - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) react-reconciler: specifier: ^0.29.0 version: 0.29.0(react@18.2.0) @@ -22,12 +22,6 @@ devDependencies: '@types/node': specifier: ^20.9.1 version: 20.9.1 - '@types/pdfkit': - specifier: ^0.13.2 - version: 0.13.2 - '@types/pdfmake': - specifier: ^0.2.8 - version: 0.2.8 '@types/react': specifier: ^18.2.37 version: 18.2.37 @@ -39,10 +33,10 @@ devDependencies: version: 0.28.8 '@typescript-eslint/eslint-plugin': specifier: ^6.10.0 - version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2) + version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.3.2) '@typescript-eslint/parser': specifier: ^6.10.0 - version: 6.10.0(eslint@8.53.0)(typescript@5.2.2) + version: 6.10.0(eslint@8.53.0)(typescript@5.3.2) '@vitejs/plugin-react-swc': specifier: ^3.5.0 version: 3.5.0(vite@5.0.0) @@ -58,15 +52,21 @@ devDependencies: prettier: specifier: ^3.1.0 version: 3.1.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) typescript: - specifier: ^5.2.2 - version: 5.2.2 + specifier: ^5.3.2 + version: 5.3.2 vite: specifier: ^5.0.0 version: 5.0.0(@types/node@20.9.1) vite-plugin-dts: specifier: ^3.6.3 - version: 3.6.3(@types/node@20.9.1)(typescript@5.2.2)(vite@5.0.0) + version: 3.6.3(@types/node@20.9.1)(typescript@5.3.2)(vite@5.0.0) vitest: specifier: ^0.34.6 version: 0.34.6 @@ -768,20 +768,19 @@ packages: resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==} dependencies: undici-types: 5.26.5 - dev: true /@types/pdfkit@0.13.2: resolution: {integrity: sha512-fmvYh4i/O6DM5za+B8TmuKxBK/iSqXQE9uWVNp/1DK2+Is+BlzhzyOtsS14TWMNwj7zxnN8u2vBl9Vs+jCX28A==} dependencies: '@types/node': 20.9.1 - dev: true + dev: false /@types/pdfmake@0.2.8: resolution: {integrity: sha512-9HavCBXKri7lhfwnM4qK012ru2qGYXvV1BVgYuNwa+vX6KFfI2Pfd0YoJ2l8m2UhE2yd8d1KuIBku6+9igDr+Q==} dependencies: '@types/node': 20.9.1 '@types/pdfkit': 0.13.2 - dev: true + dev: false /@types/prop-types@15.7.10: resolution: {integrity: sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==} @@ -815,7 +814,7 @@ packages: resolution: {integrity: sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==} dev: true - /@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2): + /@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.3.2): resolution: {integrity: sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -827,10 +826,10 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.3.2) '@typescript-eslint/scope-manager': 6.10.0 - '@typescript-eslint/type-utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/type-utils': 6.10.0(eslint@8.53.0)(typescript@5.3.2) + '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4 eslint: 8.53.0 @@ -838,13 +837,13 @@ packages: ignore: 5.3.0 natural-compare: 1.4.0 semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.2.2) - typescript: 5.2.2 + ts-api-utils: 1.0.3(typescript@5.3.2) + typescript: 5.3.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2): + /@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.3.2): resolution: {integrity: sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -856,11 +855,11 @@ packages: dependencies: '@typescript-eslint/scope-manager': 6.10.0 '@typescript-eslint/types': 6.10.0 - '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4 eslint: 8.53.0 - typescript: 5.2.2 + typescript: 5.3.2 transitivePeerDependencies: - supports-color dev: true @@ -873,7 +872,7 @@ packages: '@typescript-eslint/visitor-keys': 6.10.0 dev: true - /@typescript-eslint/type-utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): + /@typescript-eslint/type-utils@6.10.0(eslint@8.53.0)(typescript@5.3.2): resolution: {integrity: sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -883,12 +882,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) - '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.3.2) + '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.3.2) debug: 4.3.4 eslint: 8.53.0 - ts-api-utils: 1.0.3(typescript@5.2.2) - typescript: 5.2.2 + ts-api-utils: 1.0.3(typescript@5.3.2) + typescript: 5.3.2 transitivePeerDependencies: - supports-color dev: true @@ -898,7 +897,7 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.10.0(typescript@5.2.2): + /@typescript-eslint/typescript-estree@6.10.0(typescript@5.3.2): resolution: {integrity: sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -913,13 +912,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.2.2) - typescript: 5.2.2 + ts-api-utils: 1.0.3(typescript@5.3.2) + typescript: 5.3.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): + /@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.3.2): resolution: {integrity: sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -930,7 +929,7 @@ packages: '@types/semver': 7.5.5 '@typescript-eslint/scope-manager': 6.10.0 '@typescript-eslint/types': 6.10.0 - '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.3.2) eslint: 8.53.0 semver: 7.5.4 transitivePeerDependencies: @@ -1034,7 +1033,7 @@ packages: '@vue/shared': 3.3.8 dev: true - /@vue/language-core@1.8.22(typescript@5.2.2): + /@vue/language-core@1.8.22(typescript@5.3.2): resolution: {integrity: sha512-bsMoJzCrXZqGsxawtUea1cLjUT9dZnDsy5TuZ+l1fxRMzUGQUG9+Ypq4w//CqpWmrx7nIAJpw2JVF/t258miRw==} peerDependencies: typescript: '*' @@ -1049,7 +1048,7 @@ packages: computeds: 0.0.1 minimatch: 9.0.3 muggle-string: 0.3.1 - typescript: 5.2.2 + typescript: 5.3.2 vue-template-compiler: 2.7.15 dev: true @@ -2026,7 +2025,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: false /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -2116,7 +2114,6 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 - dev: false /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} @@ -2420,7 +2417,7 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: false + dev: true /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} @@ -2442,7 +2439,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -2543,7 +2539,6 @@ packages: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: loose-envify: 1.4.0 - dev: false /scope-analyzer@2.1.2: resolution: {integrity: sha512-5cfCmsTYV/wPaRIItNxatw02ua/MThdIUNnUOCYp+3LSEJvnG804ANw2VLaavNILIfWXF1D1G2KNANkBBvInwQ==} @@ -2759,13 +2754,13 @@ packages: is-number: 7.0.0 dev: true - /ts-api-utils@1.0.3(typescript@5.2.2): + /ts-api-utils@1.0.3(typescript@5.3.2): resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} engines: {node: '>=16.13.0'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.2.2 + typescript: 5.3.2 dev: true /type-check@0.3.2: @@ -2810,8 +2805,8 @@ packages: hasBin: true dev: true - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + /typescript@5.3.2: + resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -2822,7 +2817,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true /unicode-properties@1.4.1: resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} @@ -2880,7 +2874,7 @@ packages: - terser dev: true - /vite-plugin-dts@3.6.3(@types/node@20.9.1)(typescript@5.2.2)(vite@5.0.0): + /vite-plugin-dts@3.6.3(@types/node@20.9.1)(typescript@5.3.2)(vite@5.0.0): resolution: {integrity: sha512-NyRvgobl15rYj65coi/gH7UAEH+CpSjh539DbGb40DfOTZSvDLNYTzc8CK4460W+LqXuMK7+U3JAxRB3ksrNPw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -2892,12 +2886,12 @@ packages: dependencies: '@microsoft/api-extractor': 7.38.3(@types/node@20.9.1) '@rollup/pluginutils': 5.0.5 - '@vue/language-core': 1.8.22(typescript@5.2.2) + '@vue/language-core': 1.8.22(typescript@5.3.2) debug: 4.3.4 kolorist: 1.8.0 - typescript: 5.2.2 + typescript: 5.3.2 vite: 5.0.0(@types/node@20.9.1) - vue-tsc: 1.8.22(typescript@5.2.2) + vue-tsc: 1.8.22(typescript@5.3.2) transitivePeerDependencies: - '@types/node' - rollup @@ -3012,16 +3006,16 @@ packages: he: 1.2.0 dev: true - /vue-tsc@1.8.22(typescript@5.2.2): + /vue-tsc@1.8.22(typescript@5.3.2): resolution: {integrity: sha512-j9P4kHtW6eEE08aS5McFZE/ivmipXy0JzrnTgbomfABMaVKx37kNBw//irL3+LlE3kOo63XpnRigyPC3w7+z+A==} hasBin: true peerDependencies: typescript: '*' dependencies: '@volar/typescript': 1.10.10 - '@vue/language-core': 1.8.22(typescript@5.2.2) + '@vue/language-core': 1.8.22(typescript@5.3.2) semver: 7.5.4 - typescript: 5.2.2 + typescript: 5.3.2 dev: true /which@2.0.2: diff --git a/screenshots/react-devtools-demo.png b/screenshots/react-devtools-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..29f50b8c652db80850e3342c53ece262da1ea132 GIT binary patch literal 60914 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 literal 0 HcmV?d00001 diff --git a/src/PdfPreview.tsx b/src/PdfPreview.tsx index 35262a4..ab091f1 100644 --- a/src/PdfPreview.tsx +++ b/src/PdfPreview.tsx @@ -1,12 +1,38 @@ -import { FC, useEffect, useState } from "react"; -import { Content } from "pdfmake/interfaces"; +import { FC, useEffect, useMemo, useState } from "react"; +import { + BufferOptions, + TDocumentDefinitions, + TFontDictionary, +} from "pdfmake/interfaces"; import { PdfNode } from "./types/PdfNode.ts"; import pdfMake from "pdfmake/build/pdfmake"; -import { PdfRenderer } from "./PdfRenderer.ts"; +import { PdfRenderer } from "./PdfRenderer.tsx"; -export const PdfPreview: FC<{ children?: PdfNode }> = ({ children }) => { - const content = useRenderContent(children as PdfNode); - const pdfObjectUrl = usePdfObjectLink(content); +const defaultFonts: TFontDictionary = { + Roboto: { + normal: + "https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Regular.ttf", + bold: "https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Medium.ttf", + italics: + "https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Italic.ttf", + bolditalics: + "https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-MediumItalic.ttf", + }, +}; + +export interface PdfPreviewProps { + children?: PdfNode; + tableLayouts?: BufferOptions["tableLayouts"]; + fonts?: TFontDictionary; +} + +export const PdfPreview: FC = ({ + children, + tableLayouts, + fonts, +}) => { + const document = useRenderDocument(children as PdfNode); + const pdfObjectUrl = usePdfObjectLink(document, tableLayouts, fonts); return (
@@ -21,52 +47,62 @@ export const PdfPreview: FC<{ children?: PdfNode }> = ({ children }) => { ); }; -const usePdfObjectLink = (content: Content): string | null => { +const usePdfObjectLink = ( + document: TDocumentDefinitions, + tableLayouts: BufferOptions["tableLayouts"], + fonts: TFontDictionary | undefined, +): string | null => { const [link, setLink] = useState(null); - useEffect(() => { - const blobPromise = new Promise((resolve) => { - pdfMake - .createPdf( - { - content, - }, - undefined, - { - Roboto: { - normal: - "https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Regular.ttf", - bold: "https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Medium.ttf", - italics: - "https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Italic.ttf", - bolditalics: - "https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-MediumItalic.ttf", - }, - }, - ) - .getBlob(resolve); - }); + const generatePdf = useMemo( + () => + debounce((document: TDocumentDefinitions) => { + const blobPromise = new Promise((resolve) => { + pdfMake + .createPdf(document, tableLayouts, fonts ?? defaultFonts) + .getBlob(resolve); + }); - blobPromise.then((blob) => { - // console.log("New PDF rendered"); - setLink(URL.createObjectURL(blob)); - }); - }, [content]); + blobPromise.then((blob) => { + // console.log("New PDF rendered"); + setLink(URL.createObjectURL(blob)); + }); + }, 50), + [tableLayouts, fonts], + ); + + useEffect(() => { + generatePdf(document); + }, [document, generatePdf]); return link; }; -const useRenderContent = (pdfElement: PdfNode): Content => { - const [content, setContent] = useState([]); +const useRenderDocument = (pdfElement: PdfNode): TDocumentDefinitions => { + const [document, setDocument] = useState({ + content: [], + }); useEffect(() => { // console.log("Pdf has changed"); - const { unmount } = PdfRenderer.render(pdfElement, setContent); + const { unmount } = PdfRenderer.render(pdfElement, setDocument); - return () => { - unmount(); - }; + return unmount; }, [pdfElement]); - return content; + return document; }; + +function debounce( + cb: (...args: A) => void, + delay: number, +): (...args: A) => void { + let timeout: ReturnType; + + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => { + cb(...args); + }, delay); + }; +} diff --git a/src/PdfRenderer.ts b/src/PdfRenderer.ts deleted file mode 100644 index 9552655..0000000 --- a/src/PdfRenderer.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PdfNode } from "./types/PdfNode.ts"; -import ReactReconciler from "react-reconciler"; -import { hostConfig } from "./hostConfig.ts"; -import { ContentUpdateHandler } from "./types/ContentUpdateHandler.ts"; -import { Container } from "./types/Container.ts"; -import { Content } from "pdfmake/interfaces"; - -const ReactReconcilerInst = ReactReconciler(hostConfig); - -export const PdfRenderer = { - render: (reactElement: PdfNode, onUpdate: ContentUpdateHandler) => { - const container: Container = { - children: [], - onUpdate, - }; - const root = ReactReconcilerInst.createContainer( - container, - 0, - null, - true, - false, - "", - () => {}, - null, - ); - - // console.log("root", root); - - ReactReconcilerInst.updateContainer(reactElement, root, null); - ReactReconcilerInst.injectIntoDevTools({ - bundleType: 1, - version: __APP_VERSION__, - rendererPackageName: "react-pdfmake-reconciler", - }); - - const unmount = () => { - // console.log("Unmounting"); - ReactReconcilerInst.updateContainer([], root, null); - }; - - return { container, root, unmount }; - }, - renderOnce: async (renderElement: PdfNode): Promise => - new Promise((resolve) => { - const { unmount } = PdfRenderer.render(renderElement, (content) => { - resolve(content); - unmount(); - }); - }), -}; diff --git a/src/PdfRenderer.tsx b/src/PdfRenderer.tsx new file mode 100644 index 0000000..e019725 --- /dev/null +++ b/src/PdfRenderer.tsx @@ -0,0 +1,71 @@ +import { PdfNode } from "./types/PdfNode.ts"; +import { DocumentUpdateHandler } from "./types/DocumentUpdateHandler.ts"; +import { Container } from "./types/Container.ts"; +import { Content, TDocumentDefinitions } from "pdfmake/interfaces"; +import { PdfProvider } from "./components/PdfProvider.ts"; +import { createContainer } from "./createContainer.ts"; +import { ReactPdfMake } from "./ReactPdfMake.ts"; + +export const PdfRenderer = { + render: (reactElement: PdfNode, onUpdate: DocumentUpdateHandler) => { + const rootContainer: Container = createContainer({ onUpdate }); + const headerContainer: Container = createContainer(); + const footerContainer: Container = createContainer(); + + const root = ReactPdfMake.createContainer( + rootContainer, + 0, + null, + true, + false, + "", + () => {}, + null, + ); + + // console.log("root", root); + ReactPdfMake.updateContainer( + { + rootContainer.otherDocumentDefinitions = { + ...rootContainer.otherDocumentDefinitions, + ...d, + }; + onUpdate({ + ...rootContainer.otherDocumentDefinitions, + content: rootContainer.content as Content, + }); + }, + headerContainer, + footerContainer, + }} + > + {reactElement} + , + root, + null, + ); + + const unmount = () => { + // console.log("Unmounting"); + ReactPdfMake.updateContainer(null, root, null); + }; + + return { container: rootContainer, root, unmount }; + }, + renderOnce: (renderElement: PdfNode): TDocumentDefinitions => { + const { container, unmount } = PdfRenderer.render(renderElement, () => {}); + + ReactPdfMake.flushSync(); + + const content = container.content as Content; + + unmount(); + + return { + ...container.otherDocumentDefinitions, + content, + }; + }, +}; diff --git a/src/ReactPdfMake.ts b/src/ReactPdfMake.ts new file mode 100644 index 0000000..7614f2b --- /dev/null +++ b/src/ReactPdfMake.ts @@ -0,0 +1,11 @@ +import ReactReconciler from "react-reconciler"; +import { hostConfig } from "./hostConfig.ts"; + +export const ReactPdfMake = ReactReconciler(hostConfig); + +ReactPdfMake.injectIntoDevTools({ + findFiberByHostInstance: () => null, + bundleType: 1, + version: __APP_VERSION__, + rendererPackageName: "react-pdfmake-reconciler", +}); diff --git a/src/__tests__/PdfRenderer.test.tsx b/src/__tests__/PdfRenderer.test.tsx index ded4ce6..c09534c 100644 --- a/src/__tests__/PdfRenderer.test.tsx +++ b/src/__tests__/PdfRenderer.test.tsx @@ -1,16 +1,19 @@ import { describe, expect, test } from "vitest"; -import { PdfRenderer } from "../PdfRenderer.ts"; +import { PdfRenderer } from "../PdfRenderer.tsx"; +import { FC, useEffect, useState } from "react"; +import { PdfFooter, PdfHeader } from "../components"; +import { DynamicPdfNode } from "../types/DynamicPdfNode.tsx"; describe("PdfRenderer", () => { describe("PDF Make Content", () => { test("string", () => { - expect(PdfRenderer.renderOnce("Hello World!")).resolves.toEqual( + expect(PdfRenderer.renderOnce("Hello World!").content).toEqual( "Hello World!", ); }); test("number", () => { - expect(PdfRenderer.renderOnce(1)).resolves.toEqual("1"); + expect(PdfRenderer.renderOnce(1).content).toEqual("1"); }); test("text", () => { @@ -22,8 +25,8 @@ describe("PdfRenderer", () => { Hello World! Hello World! , - ), - ).resolves.toEqual([ + ).content, + ).toEqual([ { $__reactPdfMakeType: "pdf-text", text: "Hello World!", @@ -56,8 +59,8 @@ describe("PdfRenderer", () => { Hello World! , - ), - ).resolves.toEqual({ + ).content, + ).toEqual({ $__reactPdfMakeType: "pdf-columns", columns: [ "Hello World!", @@ -77,8 +80,8 @@ describe("PdfRenderer", () => { Hello World! Hello World! , - ), - ).resolves.toEqual({ + ).content, + ).toEqual({ $__reactPdfMakeType: "pdf-stack", stack: [ "Hello World!", @@ -100,8 +103,8 @@ describe("PdfRenderer", () => { Hello World! , - ), - ).resolves.toEqual({ + ).content, + ).toEqual({ $__reactPdfMakeType: "pdf-ol", ol: [ "Hello World!", @@ -124,8 +127,8 @@ describe("PdfRenderer", () => { Hello World! , - ), - ).resolves.toEqual({ + ).content, + ).toEqual({ $__reactPdfMakeType: "pdf-ul", ul: [ "Hello World!", @@ -152,8 +155,8 @@ describe("PdfRenderer", () => { , - ), - ).resolves.toEqual({ + ).content, + ).toEqual({ $__reactPdfMakeType: "pdf-table", table: { $__reactPdfMakeType: "pdf-tbody", @@ -177,8 +180,8 @@ describe("PdfRenderer", () => { expect( PdfRenderer.renderOnce( Hello World!, - ), - ).resolves.toEqual({ + ).content, + ).toEqual({ $__reactPdfMakeType: "pdf-pageReference", pageReference: "Hello World!", }); @@ -188,8 +191,8 @@ describe("PdfRenderer", () => { expect( PdfRenderer.renderOnce( Hello World!, - ), - ).resolves.toEqual({ + ).content, + ).toEqual({ $__reactPdfMakeType: "pdf-textReference", textReference: "Hello World!", }); @@ -201,8 +204,8 @@ describe("PdfRenderer", () => { Title , - ), - ).resolves.toEqual({ + ).content, + ).toEqual({ $__reactPdfMakeType: "pdf-toc", title: { $__reactPdfMakeType: "pdf-text", @@ -216,8 +219,8 @@ describe("PdfRenderer", () => { expect( PdfRenderer.renderOnce( , - ), - ).resolves.toEqual({ + ).content, + ).toEqual({ $__reactPdfMakeType: "pdf-image", image: "https://example.com/logo.png", }); @@ -225,8 +228,8 @@ describe("PdfRenderer", () => { test("svg", () => { expect( - PdfRenderer.renderOnce(), - ).resolves.toEqual({ + PdfRenderer.renderOnce().content, + ).toEqual({ $__reactPdfMakeType: "pdf-svg", svg: "", }); @@ -234,8 +237,8 @@ describe("PdfRenderer", () => { test("qr", () => { expect( - PdfRenderer.renderOnce(), - ).resolves.toEqual({ + PdfRenderer.renderOnce().content, + ).toEqual({ $__reactPdfMakeType: "pdf-qr", qr: "Hello World!", }); @@ -245,8 +248,8 @@ describe("PdfRenderer", () => { expect( PdfRenderer.renderOnce( , - ), - ).resolves.toEqual({ + ).content, + ).toEqual({ $__reactPdfMakeType: "pdf-canvas", canvas: [{ type: "rect", x: 0, y: 0, w: 10, h: 10 }], }); @@ -254,8 +257,86 @@ describe("PdfRenderer", () => { test("array", () => { expect( - PdfRenderer.renderOnce(Hello World!), - ).resolves.toEqual(["Hello World!"]); + PdfRenderer.renderOnce(Hello World!).content, + ).toEqual(["Hello World!"]); + }); + }); + + describe("renderOnce", () => { + const Test: FC = () => { + const [text, setText] = useState(""); + + useEffect(() => { + setText("Hello"); + }, []); + + return <>{text}; + }; + + test("flushes effects", () => { + expect(PdfRenderer.renderOnce().content).toEqual("Hello"); + }); + }); + + describe("margin content", () => { + test("static header content", () => { + expect( + PdfRenderer.renderOnce(Hello).header, + ).toEqual("Hello"); + }); + + test("static footer content", () => { + expect( + PdfRenderer.renderOnce(Hello).footer, + ).toEqual("Hello"); + }); + + test("dynamic header content", () => { + const document = PdfRenderer.renderOnce( + + {(pageNumber, pageCount, pageSize) => ( + + {pageNumber} + {pageCount} + {pageSize.width} + + )} + , + ); + expect( + (document.header as DynamicPdfNode)(1, 2, { + width: 3, + height: 4, + orientation: "portrait", + }), + ).toEqual({ + $__reactPdfMakeType: "pdf-text", + text: ["1", "2", "3"], + }); + }); + + test("dynamic footer content", () => { + const document = PdfRenderer.renderOnce( + + {(pageNumber, pageCount, pageSize) => ( + + {pageNumber} + {pageCount} + {pageSize.width} + + )} + , + ); + expect( + (document.footer as DynamicPdfNode)(1, 2, { + width: 3, + height: 4, + orientation: "portrait", + }), + ).toEqual({ + $__reactPdfMakeType: "pdf-text", + text: ["1", "2", "3"], + }); }); }); }); diff --git a/src/components/PdfDocument.tsx b/src/components/PdfDocument.tsx new file mode 100644 index 0000000..4f1a8a2 --- /dev/null +++ b/src/components/PdfDocument.tsx @@ -0,0 +1,20 @@ +import { FC, memo, ReactNode, useEffect } from "react"; +import { TDocumentDefinitions } from "pdfmake/interfaces"; +import { usePdfContext } from "./PdfProvider.ts"; + +type PdfDocumentProps = Omit & { + children?: ReactNode; +}; +const BasePdfDocument: FC = ({ children, ...props }) => { + const { updateDocumentDefinitions } = usePdfContext(); + + useEffect(() => { + updateDocumentDefinitions(props); + }, [props, updateDocumentDefinitions]); + + return children; +}; + +BasePdfDocument.displayName = "PdfDocument"; + +export const PdfDocument = memo(BasePdfDocument); diff --git a/src/components/PdfFooter.tsx b/src/components/PdfFooter.tsx new file mode 100644 index 0000000..4957984 --- /dev/null +++ b/src/components/PdfFooter.tsx @@ -0,0 +1,7 @@ +import { withPdfMarginContent } from "./withPdfMarginContent.tsx"; + +export const PdfFooter = withPdfMarginContent( + "footerContainer", + "footer", + "PdfFooter", +); diff --git a/src/components/PdfHeader.tsx b/src/components/PdfHeader.tsx new file mode 100644 index 0000000..513eb6c --- /dev/null +++ b/src/components/PdfHeader.tsx @@ -0,0 +1,7 @@ +import { withPdfMarginContent } from "./withPdfMarginContent.tsx"; + +export const PdfHeader = withPdfMarginContent( + "headerContainer", + "header", + "PdfHeader", +); diff --git a/src/components/PdfProvider.ts b/src/components/PdfProvider.ts new file mode 100644 index 0000000..8446574 --- /dev/null +++ b/src/components/PdfProvider.ts @@ -0,0 +1,24 @@ +import { TDocumentDefinitions } from "pdfmake/interfaces"; +import { createContext, useContext } from "react"; +import { Container } from "../types/Container.ts"; +import { createContainer } from "../createContainer.ts"; + +export interface PdfContextType { + updateDocumentDefinitions: ( + documentDefinitions: Partial, + ) => void; + headerContainer: Container; + footerContainer: Container; +} + +export const PdfContext = createContext({ + updateDocumentDefinitions: () => {}, + headerContainer: createContainer(), + footerContainer: createContainer(), +}); + +PdfContext.displayName = "PdfContext"; + +export const PdfProvider = PdfContext.Provider; + +export const usePdfContext = (): PdfContextType => useContext(PdfContext); diff --git a/src/components/index.ts b/src/components/index.ts index 2cfd9f0..0dcd95a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1 +1,4 @@ export * from "./PdfTable.tsx"; +export * from "./PdfDocument.tsx"; +export * from "./PdfHeader.tsx"; +export * from "./PdfFooter.tsx"; diff --git a/src/components/withPdfMarginContent.tsx b/src/components/withPdfMarginContent.tsx new file mode 100644 index 0000000..faf36fd --- /dev/null +++ b/src/components/withPdfMarginContent.tsx @@ -0,0 +1,63 @@ +import { FC, ReactPortal, useEffect } from "react"; +import { PdfNode } from "../types/PdfNode.ts"; +import { DynamicPdfNode } from "../types/DynamicPdfNode.tsx"; +import { PdfContextType, usePdfContext } from "./PdfProvider.ts"; +import { PdfRenderer } from "../PdfRenderer.tsx"; +import { + Content, + DynamicContent, + TDocumentDefinitions, +} from "pdfmake/interfaces"; +import { Container } from "../types/Container.ts"; +import { ReactPdfMake } from "../ReactPdfMake.ts"; + +export interface PdfMarginContentProps { + children: PdfNode | DynamicPdfNode; +} + +type OnlyContainerKeys = { + [K in keyof T]: Container extends T[K] ? K : never; +}[keyof T]; + +type OnlyDynamicContentKeys = { + [K in keyof Required]: Content | DynamicContent extends T[K] ? K : never; +}[keyof T]; + +export const withPdfMarginContent = ( + containerKey: OnlyContainerKeys, + documentContentKey: OnlyDynamicContentKeys, + displayName: string, +) => { + const PdfMargin: FC = ({ children }) => { + const { updateDocumentDefinitions, [containerKey]: marginContainer } = + usePdfContext(); + + useEffect(() => { + if (typeof children === "function") { + updateDocumentDefinitions({ + [documentContentKey]: (...args: Parameters) => + PdfRenderer.renderOnce( + typeof children === "function" ? children(...args) : children, + ).content, + }); + } else { + updateDocumentDefinitions({ + [documentContentKey]: marginContainer.content as Content, + }); + } + }, [children, marginContainer.content, updateDocumentDefinitions]); + + return typeof children === "function" + ? null + : (ReactPdfMake.createPortal( + children, + marginContainer, + null, + null, + ) as unknown as ReactPortal); + }; + + PdfMargin.displayName = displayName; + + return PdfMargin; +}; diff --git a/src/createContainer.ts b/src/createContainer.ts new file mode 100644 index 0000000..80c4eab --- /dev/null +++ b/src/createContainer.ts @@ -0,0 +1,10 @@ +import { Container } from "./types/Container.ts"; + +export const createContainer = ( + container: Partial = {}, +): Container => ({ + content: [], + onUpdate: () => {}, + otherDocumentDefinitions: {}, + ...container, +}); diff --git a/src/demo/App.tsx b/src/demo/App.tsx index d290452..fc50f78 100644 --- a/src/demo/App.tsx +++ b/src/demo/App.tsx @@ -9,7 +9,7 @@ import { import "./App.css"; import { PdfPreview } from "../PdfPreview.tsx"; import { PdfNode } from "../types/PdfNode.ts"; -import { PdfTable } from "../components"; +import { PdfDocument, PdfFooter, PdfHeader, PdfTable } from "../components"; import { Heading } from "./components/Heading.tsx"; const Bold: FC<{ children?: PdfNode }> = ({ children }) => { @@ -71,46 +71,70 @@ function App() { {shown && ( - - Report for {name.length === 0 ? "No Name" : name} - - - - - - Hello Google - - Check this out - - Hello - Hello - Hello - - - - - Hello - Hello - - - Hello - Hi - - - Hello - + + + Report for {name.length === 0 ? "No Name" : name} + + + + + + Hello{" "} + Google + + Check this out + + Hello + Hello + Hello + + + + Hello - - - - - Hello, Hello], - [Hello, Hello], - ]} - /> + Hello + + + Hello + Hi + + + Hello + + Hello + + + + + Hello, Hello], + [Hello, Hello], + ]} + /> + + + {(pageNumber, pageCount) => ( + + <>This is a footer + + Page {pageNumber} / {pageCount} + + + )} + + + + This is a header! + + )} diff --git a/src/hostConfig.ts b/src/hostConfig.ts index 1f581ce..e4f11ab 100644 --- a/src/hostConfig.ts +++ b/src/hostConfig.ts @@ -63,9 +63,7 @@ export const hostConfig: HostConfig< getPublicInstance: (instance) => instance, prepareForCommit: () => null, resetAfterCommit: () => {}, - preparePortalMount: () => { - console.error("Portal is not supported"); - }, + preparePortalMount: () => {}, scheduleTimeout: globalThis.setTimeout, cancelTimeout: globalThis.clearTimeout, beforeActiveInstanceBlur: () => {}, @@ -113,8 +111,11 @@ export const hostConfig: HostConfig< // console.log("finalizeContainerChildren", container, newChildren); const newContainerChildren: PdfReconcilerNode = newChildren.length === 1 ? newChildren[0] : newChildren; - container.children = newContainerChildren; - container.onUpdate(newContainerChildren as Content); + container.content = newContainerChildren; + container.onUpdate({ + ...container.otherDocumentDefinitions, + content: newContainerChildren as Content, + }); }, replaceContainerChildren: () => { /* noop */ diff --git a/src/index.ts b/src/index.ts index 666bf83..6b91ef3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export * from "./PdfPreview.tsx"; -export { PdfRenderer } from "./PdfRenderer.ts"; +export { PdfRenderer } from "./PdfRenderer.tsx"; export * from "./components"; +export * from "./ReactPdfMake.ts"; diff --git a/src/types/Container.ts b/src/types/Container.ts index 386efdd..c59f333 100644 --- a/src/types/Container.ts +++ b/src/types/Container.ts @@ -1,7 +1,9 @@ -import { ContentUpdateHandler } from "./ContentUpdateHandler.ts"; +import { DocumentUpdateHandler } from "./DocumentUpdateHandler.ts"; import { PdfReconcilerNode } from "./PdfElements.ts"; +import { TDocumentDefinitions } from "pdfmake/interfaces"; export interface Container { - children: PdfReconcilerNode; - onUpdate: ContentUpdateHandler; + content: PdfReconcilerNode; + onUpdate: DocumentUpdateHandler; + otherDocumentDefinitions: Omit; } diff --git a/src/types/ContentUpdateHandler.ts b/src/types/ContentUpdateHandler.ts deleted file mode 100644 index 2656188..0000000 --- a/src/types/ContentUpdateHandler.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Content } from "pdfmake/interfaces"; - -export type ContentUpdateHandler = (content: Content) => void; diff --git a/src/types/DocumentUpdateHandler.ts b/src/types/DocumentUpdateHandler.ts new file mode 100644 index 0000000..d243fd6 --- /dev/null +++ b/src/types/DocumentUpdateHandler.ts @@ -0,0 +1,3 @@ +import { TDocumentDefinitions } from "pdfmake/interfaces"; + +export type DocumentUpdateHandler = (content: TDocumentDefinitions) => void; diff --git a/src/types/DynamicPdfNode.tsx b/src/types/DynamicPdfNode.tsx new file mode 100644 index 0000000..d09505f --- /dev/null +++ b/src/types/DynamicPdfNode.tsx @@ -0,0 +1,4 @@ +import { DynamicContent } from "pdfmake/interfaces"; +import { PdfNode } from "./PdfNode.ts"; + +export type DynamicPdfNode = (...args: Parameters) => PdfNode;