From 8e582d928eae77ba5825465cedf16b42a0ac8886 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 5 Sep 2025 18:12:01 +0200 Subject: [PATCH 1/4] Updated exporter examples --- .../05-converting-blocks-to-pdf/README.md | 2 +- .../05-converting-blocks-to-pdf/src/App.tsx | 67 ++++++++-------- .../src/styles.css | 39 ++++----- .../06-converting-blocks-to-docx/README.md | 4 +- .../06-converting-blocks-to-docx/index.html | 2 +- .../06-converting-blocks-to-docx/src/App.tsx | 31 ++++--- .../src/styles.css | 32 ++++---- .../07-converting-blocks-to-odt/README.md | 4 +- .../07-converting-blocks-to-odt/index.html | 2 +- .../07-converting-blocks-to-odt/src/App.tsx | 34 ++++---- .../src/styles.css | 32 ++++---- .../.bnexample.json | 3 +- .../README.md | 6 +- .../index.html | 2 +- .../package.json | 3 +- .../src/App.tsx | 80 ++++++++++--------- .../src/styles.css | 45 ++++------- playground/src/examples.gen.tsx | 17 ++-- pnpm-lock.yaml | 5 +- 19 files changed, 186 insertions(+), 224 deletions(-) diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/README.md b/examples/05-interoperability/05-converting-blocks-to-pdf/README.md index 63cafa464d..38286e00ef 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/README.md +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/README.md @@ -2,4 +2,4 @@ This example exports the current document (all blocks) as an PDF file and downloads it to your computer. -**Try it out:** Edit the document and click "Download .pdf" in top-left corner, to download the PDF file. +**Try it out:** Edit the document and click "Download .pdf" at the top to download the PDF file. diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx b/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx index af0309d209..27e4653cb3 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx @@ -24,30 +24,29 @@ import { locales as multiColumnLocales, withMultiColumn, } from "@blocknote/xl-multi-column"; -import { PDFViewer } from "@react-pdf/renderer"; -import { useEffect, useMemo, useReducer, useState } from "react"; +import { pdf } from "@react-pdf/renderer"; +import { JSX, useMemo, useState } from "react"; import "./styles.css"; export default function App() { - // Stores the editor's contents as HTML. - const [pdfDocument, setPDFDocument] = useState(); - const [renders, forceRerender] = useReducer((s) => s + 1, 0); - - // Creates a new editor instance with some initial content. + // Creates a new editor instance. const editor = useCreateBlockNote({ + // Adds support for page breaks & multi-column blocks. schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())), dropCursor: multiColumnDropCursor, dictionary: { ...locales.en, multi_column: multiColumnLocales.en, }, + // Adds support for advanced table features. tables: { splitCells: true, cellBackgroundColor: true, cellTextColor: true, headers: true, }, + // Sets initial editor content. initialContent: [ { type: "paragraph", @@ -381,6 +380,8 @@ export default function App() { }, ], }); + + // Additional Slash Menu items for page breaks and multi-column blocks. const getSlashMenuItems = useMemo(() => { return async (query: string) => filterSuggestionItems( @@ -392,37 +393,37 @@ export default function App() { query, ); }, [editor]); - const onChange = async () => { + + // Exports the editor content to PDF and downloads it. + const onDownloadClick = async () => { const exporter = new PDFExporter(editor.schema, pdfDefaultSchemaMappings); - // Converts the editor's contents from Block objects to HTML and store to state. - const pdfDocument = await exporter.toReactPDFDocument(editor.document); - setPDFDocument(pdfDocument); - forceRerender(); + const pdfDocument: JSX.Element = await exporter.toReactPDFDocument( + editor.document, + ); + const blob = await pdf(pdfDocument).toBlob(); - // const blob = await ReactPDF.pdf(pdfDocument).toBlob(); + const link = document.createElement("a"); + link.href = window.URL.createObjectURL(blob); + link.download = "My Document (blocknote export).pdf"; + document.body.appendChild(link); + link.dispatchEvent( + new MouseEvent("click", { + bubbles: true, + cancelable: true, + view: window, + }), + ); + link.remove(); + window.URL.revokeObjectURL(link.href); }; - useEffect(() => { - onChange(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Renders the editor instance, and its contents as HTML below. + // Renders the editor instance, and a download button above. return ( -
-
- - - -
-
- - {pdfDocument} - -
+
+ +
); } diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/src/styles.css b/examples/05-interoperability/05-converting-blocks-to-pdf/src/styles.css index f41ba2a5c4..9508ea8db3 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/src/styles.css +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/src/styles.css @@ -1,30 +1,21 @@ -.wrapper { +.download-wrapper { + align-items: center; display: flex; - flex-direction: row; - height: 100%; + flex-direction: column; + gap: 8px; + padding: 8px; + width: 100%; } -@media (max-width: 800px) { - .wrapper { - flex-direction: column; - } - - .editor { - max-height: 500px; - overflow-y: scroll; - } -} - -.wrapper > div { - flex: 1; -} - -.pdf { - min-height: 500px; - display: flex; - align-items: stretch; +.download-button { + background-color: #0090ff; + border: none; + border-radius: 4px; + color: white; + cursor: pointer; + width: 100%; } -.editor.bordered { - border: 1px solid gray; +.download-button:hover { + background-color: #30b0ff; } diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/README.md b/examples/05-interoperability/06-converting-blocks-to-docx/README.md index 2a826547bb..abbecc92d5 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/README.md +++ b/examples/05-interoperability/06-converting-blocks-to-docx/README.md @@ -1,5 +1,5 @@ -# Exporting documents to .docx (Office Open XML) +# Exporting documents to DOCX (Office Open XML) This example exports the current document (all blocks) as an Microsoft Word Document (DOCX) file and downloads it to your computer. -**Try it out:** Edit the document and click "Download .docx" in top-left corner, to download the DOCX file. +**Try it out:** Edit the document and click "Download .docx" at the top to download the DOCX file. diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/index.html b/examples/05-interoperability/06-converting-blocks-to-docx/index.html index 28135d372d..a9eb3589ef 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/index.html +++ b/examples/05-interoperability/06-converting-blocks-to-docx/index.html @@ -2,7 +2,7 @@ - Exporting documents to .docx (Office Open XML) + Exporting documents to DOCX (Office Open XML) diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx b/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx index 597f3793b7..aaf6153802 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx +++ b/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx @@ -29,20 +29,23 @@ import { useMemo } from "react"; import "./styles.css"; export default function App() { - // Creates a new editor instance with some initial content. + // Creates a new editor instance. const editor = useCreateBlockNote({ + // Adds support for page breaks & multi-column blocks. schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())), dropCursor: multiColumnDropCursor, dictionary: { ...locales.en, multi_column: multiColumnLocales.en, }, + // Adds support for advanced table features. tables: { splitCells: true, cellBackgroundColor: true, cellTextColor: true, headers: true, }, + // Sets initial editor content. initialContent: [ { type: "paragraph", @@ -377,6 +380,8 @@ export default function App() { }, ], }); + + // Additional Slash Menu items for page breaks and multi-column blocks. const getSlashMenuItems = useMemo(() => { return async (query: string) => filterSuggestionItems( @@ -388,9 +393,10 @@ export default function App() { query, ); }, [editor]); + + // Exports the editor content to DOCX and downloads it. const onDownloadClick = async () => { const exporter = new DOCXExporter(editor.schema, docxDefaultSchemaMappings); - const blob = await exporter.toBlob(editor.document); const link = document.createElement("a"); @@ -408,22 +414,13 @@ export default function App() { window.URL.revokeObjectURL(link.href); }; - // Renders the editor instance, and its contents as HTML below. + // Renders the editor instance, and a download button above. return ( -
-
- -
-
- - - -
+
+ +
); } diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/src/styles.css b/examples/05-interoperability/06-converting-blocks-to-docx/src/styles.css index 6d5eeba7fe..9508ea8db3 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/src/styles.css +++ b/examples/05-interoperability/06-converting-blocks-to-docx/src/styles.css @@ -1,25 +1,21 @@ -.wrapper { +.download-wrapper { + align-items: center; display: flex; flex-direction: column; - height: 100%; -} - -.item { - border-radius: 0.5rem; - flex: 1; - overflow: hidden; + gap: 8px; + padding: 8px; + width: 100%; } -.item.bordered { - border: 1px solid gray; +.download-button { + background-color: #0090ff; + border: none; + border-radius: 4px; + color: white; + cursor: pointer; + width: 100%; } -.item pre { - border-radius: 0.5rem; - height: 100%; - overflow: auto; - padding-block: 1rem; - padding-inline: 54px; - width: 100%; - white-space: pre-wrap; +.download-button:hover { + background-color: #30b0ff; } diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/README.md b/examples/05-interoperability/07-converting-blocks-to-odt/README.md index 8b7c159e30..2ebda4547d 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/README.md +++ b/examples/05-interoperability/07-converting-blocks-to-odt/README.md @@ -1,5 +1,5 @@ -# Exporting documents to .odt (Open Document Text) +# Exporting documents to ODT (Open Document Text) This example exports the current document (all blocks) as an Open Document Text (ODT) file and downloads it to your computer. -**Try it out:** Edit the document and click "Download .odt" in top-left corner, to download the ODT file. +**Try it out:** Edit the document and click "Download .odt" at the top to download the ODT file. diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/index.html b/examples/05-interoperability/07-converting-blocks-to-odt/index.html index aa0611a99f..175d808b5e 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/index.html +++ b/examples/05-interoperability/07-converting-blocks-to-odt/index.html @@ -2,7 +2,7 @@ - Exporting documents to .odt (Open Document Text) + Exporting documents to ODT (Open Document Text) diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx b/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx index b346e939a1..d05dea1f7f 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx +++ b/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx @@ -29,19 +29,23 @@ import { useMemo } from "react"; import "./styles.css"; export default function App() { - // Creates a new editor instance with some initial content. + // Creates a new editor instance. const editor = useCreateBlockNote({ + // Adds support for page breaks & multi-column blocks. schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())), dropCursor: multiColumnDropCursor, dictionary: { ...locales.en, multi_column: multiColumnLocales.en, }, + // Adds support for advanced table features. tables: { splitCells: true, - cellTextColor: true, cellBackgroundColor: true, + cellTextColor: true, + headers: true, }, + // Sets initial editor content. initialContent: [ { type: "paragraph", @@ -375,6 +379,8 @@ export default function App() { }, ], }); + + // Additional Slash Menu items for page breaks and multi-column blocks. const getSlashMenuItems = useMemo(() => { return async (query: string) => filterSuggestionItems( @@ -386,9 +392,10 @@ export default function App() { query, ); }, [editor]); + + // Exports the editor content to ODT and downloads it. const onDownloadClick = async () => { const exporter = new ODTExporter(editor.schema, odtDefaultSchemaMappings); - const blob = await exporter.toODTDocument(editor.document); const link = document.createElement("a"); @@ -406,22 +413,13 @@ export default function App() { window.URL.revokeObjectURL(link.href); }; - // Renders the editor instance, and its contents as HTML below. + // Renders the editor instance, and a download button above. return ( -
-
- -
-
- - - -
+
+ +
); } diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/src/styles.css b/examples/05-interoperability/07-converting-blocks-to-odt/src/styles.css index 6d5eeba7fe..9508ea8db3 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/src/styles.css +++ b/examples/05-interoperability/07-converting-blocks-to-odt/src/styles.css @@ -1,25 +1,21 @@ -.wrapper { +.download-wrapper { + align-items: center; display: flex; flex-direction: column; - height: 100%; -} - -.item { - border-radius: 0.5rem; - flex: 1; - overflow: hidden; + gap: 8px; + padding: 8px; + width: 100%; } -.item.bordered { - border: 1px solid gray; +.download-button { + background-color: #0090ff; + border: none; + border-radius: 4px; + color: white; + cursor: pointer; + width: 100%; } -.item pre { - border-radius: 0.5rem; - height: 100%; - overflow: auto; - padding-block: 1rem; - padding-inline: 54px; - width: 100%; - white-space: pre-wrap; +.download-button:hover { + background-color: #30b0ff; } diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json b/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json index 4292fc5c25..948f9f1e8e 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json @@ -4,8 +4,7 @@ "author": "jmarbutt", "tags": [""], "dependencies": { - "@blocknote/xl-email-exporter": "latest", - "@react-email/render": "^1.1.2" + "@blocknote/xl-email-exporter": "latest" }, "pro": true } diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/README.md b/examples/05-interoperability/08-converting-blocks-to-react-email/README.md index 39cad64a26..68d301ade4 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/README.md +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/README.md @@ -1,5 +1,5 @@ -# Exporting documents to React Email +# Exporting documents to Email (HTML) -This example exports the current document (all blocks) as a React Email document. +This example exports the current document (all blocks) as an HTML file for use in emails, and downloads it to your computer. -**Try it out:** Edit the document and the preview will update. +**Try it out:** Edit the document and click "Download email .html" at the top to download the HTML file. diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/index.html b/examples/05-interoperability/08-converting-blocks-to-react-email/index.html index d06d43c35d..a8759093bb 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/index.html +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/index.html @@ -2,7 +2,7 @@ - Exporting documents to React Email + Exporting documents to Email (HTML) diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json index d1d34a8967..f10da74e28 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json @@ -17,8 +17,7 @@ "@blocknote/shadcn": "latest", "react": "^19.1.0", "react-dom": "^19.1.0", - "@blocknote/xl-email-exporter": "latest", - "@react-email/render": "^1.1.2" + "@blocknote/xl-email-exporter": "latest" }, "devDependencies": { "@types/react": "^19.1.0", diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/src/App.tsx b/examples/05-interoperability/08-converting-blocks-to-react-email/src/App.tsx index 6b70999d78..cfd1feacbc 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/src/App.tsx +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/src/App.tsx @@ -12,6 +12,7 @@ import "@blocknote/mantine/style.css"; import { SuggestionMenuController, getDefaultReactSlashMenuItems, + getPageBreakReactSlashMenuItems, useBlockNoteContext, useCreateBlockNote, usePrefersColorScheme, @@ -20,23 +21,23 @@ import { ReactEmailExporter, reactEmailDefaultSchemaMappings, } from "@blocknote/xl-email-exporter"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo } from "react"; import "./styles.css"; export default function App() { - // Stores the editor's contents as HTML. - const [emailDocument, setEmailDocument] = useState(); - - // Creates a new editor instance with some initial content. + // Creates a new editor instance. const editor = useCreateBlockNote({ + // Adds support for page breaks. schema: withPageBreak(BlockNoteSchema.create()), + // Adds support for advanced table features. tables: { splitCells: true, cellBackgroundColor: true, cellTextColor: true, headers: true, }, + // Sets initial editor content. initialContent: [ { type: "paragraph", @@ -318,15 +319,22 @@ export default function App() { ], }); + // Additional Slash Menu items for page breaks. + const slashMenuItems = useMemo(() => { + return combineByGroup( + getDefaultReactSlashMenuItems(editor), + getPageBreakReactSlashMenuItems(editor), + ); + }, [editor]); + const existingContext = useBlockNoteContext(); const systemColorScheme = usePrefersColorScheme(); - const colorScheme = - existingContext?.colorSchemePreference || systemColorScheme; - const onChange = async () => { - if (!editor || !editor.document) { - return; - } + // Exports the editor content to HTML (for email) and downloads it. + const onDownloadClick = async () => { + const colorScheme = + existingContext?.colorSchemePreference || systemColorScheme; + const exporter = new ReactEmailExporter( editor.schema, reactEmailDefaultSchemaMappings, @@ -335,37 +343,33 @@ export default function App() { colorScheme === "dark" ? COLORS_DARK_MODE_DEFAULT : COLORS_DEFAULT, }, ); - const emailHtml = await exporter.toReactEmailDocument(editor.document); + const blob = new Blob( + [await exporter.toReactEmailDocument(editor.document)], + { type: "text/html" }, + ); - setEmailDocument(emailHtml); + const link = document.createElement("a"); + link.href = window.URL.createObjectURL(blob); + link.download = "My Document (blocknote export).html"; + document.body.appendChild(link); + link.dispatchEvent( + new MouseEvent("click", { + bubbles: true, + cancelable: true, + view: window, + }), + ); + link.remove(); + window.URL.revokeObjectURL(link.href); }; - useEffect(() => { - onChange(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const slashMenuItems = useMemo(() => { - return combineByGroup(getDefaultReactSlashMenuItems(editor)); - }, [editor]); - - // Renders the editor instance, and its contents as HTML below. + // Renders the editor instance, and a download button above. return ( -
-
- - - filterSuggestionItems(slashMenuItems, query) - } - /> - -
-
+
+ +
); } diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/src/styles.css b/examples/05-interoperability/08-converting-blocks-to-react-email/src/styles.css index 1ccbb9d746..9508ea8db3 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/src/styles.css +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/src/styles.css @@ -1,36 +1,21 @@ -.wrapper { +.download-wrapper { + align-items: center; display: flex; - flex-direction: row; - height: 100%; + flex-direction: column; + gap: 8px; + padding: 8px; + width: 100%; } -@media (max-width: 800px) { - .wrapper { - flex-direction: column; - } - - .editor { - max-height: 500px; - overflow-y: scroll; - } -} - -.wrapper > div { - flex: 1; -} - -.email { - min-height: 500px; -} - -/* hack to get react-email to show on website */ -@tailwind base; -@layer base { - .email [hidden]:where(:not([hidden="until-found"])) { - display: block !important; - } +.download-button { + background-color: #0090ff; + border: none; + border-radius: 4px; + color: white; + cursor: pointer; + width: 100%; } -.editor.bordered { - border: 1px solid gray; +.download-button:hover { + background-color: #30b0ff; } diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 131ade6a07..5bdea13339 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -1045,7 +1045,7 @@ "pathFromRoot": "examples/05-interoperability", "slug": "interoperability" }, - "readme": "This example exports the current document (all blocks) as an PDF file and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download .pdf\" in top-left corner, to download the PDF file." + "readme": "This example exports the current document (all blocks) as an PDF file and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download .pdf\" at the top to download the PDF file." }, { "projectSlug": "converting-blocks-to-docx", @@ -1065,12 +1065,12 @@ } as any, "pro": true }, - "title": "Exporting documents to .docx (Office Open XML)", + "title": "Exporting documents to DOCX (Office Open XML)", "group": { "pathFromRoot": "examples/05-interoperability", "slug": "interoperability" }, - "readme": "This example exports the current document (all blocks) as an Microsoft Word Document (DOCX) file and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download .docx\" in top-left corner, to download the DOCX file." + "readme": "This example exports the current document (all blocks) as an Microsoft Word Document (DOCX) file and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download .docx\" at the top to download the DOCX file." }, { "projectSlug": "converting-blocks-to-odt", @@ -1089,12 +1089,12 @@ } as any, "pro": true }, - "title": "Exporting documents to .odt (Open Document Text)", + "title": "Exporting documents to ODT (Open Document Text)", "group": { "pathFromRoot": "examples/05-interoperability", "slug": "interoperability" }, - "readme": "This example exports the current document (all blocks) as an Open Document Text (ODT) file and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download .odt\" in top-left corner, to download the ODT file." + "readme": "This example exports the current document (all blocks) as an Open Document Text (ODT) file and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download .odt\" at the top to download the ODT file." }, { "projectSlug": "converting-blocks-to-react-email", @@ -1108,17 +1108,16 @@ "" ], "dependencies": { - "@blocknote/xl-email-exporter": "latest", - "@react-email/render": "^1.1.2" + "@blocknote/xl-email-exporter": "latest" } as any, "pro": true }, - "title": "Exporting documents to React Email", + "title": "Exporting documents to Email (HTML)", "group": { "pathFromRoot": "examples/05-interoperability", "slug": "interoperability" }, - "readme": "This example exports the current document (all blocks) as a React Email document.\n\n**Try it out:** Edit the document and the preview will update." + "readme": "This example exports the current document (all blocks) as an HTML file for use in emails, and downloads it to your computer.\n\n**Try it out:** Edit the document and click \"Download email .html\" at the top to download the HTML file." } ] }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7d7c26e08..f6a14334d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2344,9 +2344,6 @@ importers: '@blocknote/xl-email-exporter': specifier: latest version: link:../../../packages/xl-email-exporter - '@react-email/render': - specifier: ^1.1.2 - version: 1.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.1.0 version: 19.1.0 @@ -23107,7 +23104,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.15.2 + '@types/node': 20.17.50 merge-stream: 2.0.0 supports-color: 8.1.1 From a12d8c507751f92f4257fd29d2f8c228933cb332 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 9 Sep 2025 11:20:00 +0200 Subject: [PATCH 2/4] Implemented PR feedback --- .../05-converting-blocks-to-pdf/src/App.tsx | 72 ++++++++++++---- .../src/styles.css | 14 ++++ .../06-converting-blocks-to-docx/src/App.tsx | 7 +- .../07-converting-blocks-to-odt/src/App.tsx | 7 +- .../.bnexample.json | 3 +- .../package.json | 3 +- .../src/App.tsx | 82 ++++++++++++++----- .../src/styles.css | 14 ++++ playground/src/examples.gen.tsx | 3 +- pnpm-lock.yaml | 3 + 10 files changed, 168 insertions(+), 40 deletions(-) diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx b/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx index 27e4653cb3..7acf02b4c7 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx @@ -24,12 +24,19 @@ import { locales as multiColumnLocales, withMultiColumn, } from "@blocknote/xl-multi-column"; -import { pdf } from "@react-pdf/renderer"; -import { JSX, useMemo, useState } from "react"; +import { pdf, PDFViewer } from "@react-pdf/renderer"; +import { JSX, useEffect, useMemo, useState } from "react"; import "./styles.css"; export default function App() { + // Stores the editor's contents as JSX for download and displaying the PDF + // using ReactPDF's `PDFViewer` component. + const [pdfDocument, setPDFDocument] = useState(); + + // Toggles between editor and PDF view. + const [show, setShow] = useState<"editor" | "pdf">("editor"); + // Creates a new editor instance. const editor = useCreateBlockNote({ // Adds support for page breaks & multi-column blocks. @@ -382,8 +389,8 @@ export default function App() { }); // Additional Slash Menu items for page breaks and multi-column blocks. - const getSlashMenuItems = useMemo(() => { - return async (query: string) => + const getSlashMenuItems = useMemo( + () => async (query: string) => filterSuggestionItems( combineByGroup( getDefaultReactSlashMenuItems(editor), @@ -391,15 +398,24 @@ export default function App() { getMultiColumnSlashMenuItems(editor), ), query, - ); - }, [editor]); + ), + [editor], + ); - // Exports the editor content to PDF and downloads it. - const onDownloadClick = async () => { + // Exports the editor document to PDF whenever it changes. + const onChange = async () => { const exporter = new PDFExporter(editor.schema, pdfDefaultSchemaMappings); - const pdfDocument: JSX.Element = await exporter.toReactPDFDocument( - editor.document, - ); + const pdfDocument = await exporter.toReactPDFDocument(editor.document); + setPDFDocument(pdfDocument); + }; + + // Exports the inital editor document to PDF. + useEffect(() => { + onChange(); + }, []); + + // Downloads the PDF. + const onDownloadClick = async () => { const blob = await pdf(pdfDocument).toBlob(); const link = document.createElement("a"); @@ -417,13 +433,37 @@ export default function App() { window.URL.revokeObjectURL(link.href); }; - // Renders the editor instance, and a download button above. + // Renders the editor instance or PDF view. Also renders two buttons above + // for switching between views and downloading the PDF. return (
- - +
+ + +
+
+ {show === "editor" ? ( + + + + ) : ( + + {pdfDocument} + + )} +
); } diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/src/styles.css b/examples/05-interoperability/05-converting-blocks-to-pdf/src/styles.css index 9508ea8db3..54a75f0b6c 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/src/styles.css +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/src/styles.css @@ -3,10 +3,24 @@ display: flex; flex-direction: column; gap: 8px; + height: 100%; padding: 8px; width: 100%; } +.download-buttons { + display: flex; + gap: 8px; + width: 100%; +} + +.download-view { + flex: 1; + height: 0; + overflow: auto; + width: 100%; +} + .download-button { background-color: #0090ff; border: none; diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx b/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx index aaf6153802..0aaf40e13b 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx +++ b/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx @@ -420,7 +420,12 @@ export default function App() { - + + +
); } diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx b/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx index d05dea1f7f..335a2c6402 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx +++ b/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx @@ -419,7 +419,12 @@ export default function App() { - + + +
); } diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json b/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json index 948f9f1e8e..4292fc5c25 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json @@ -4,7 +4,8 @@ "author": "jmarbutt", "tags": [""], "dependencies": { - "@blocknote/xl-email-exporter": "latest" + "@blocknote/xl-email-exporter": "latest", + "@react-email/render": "^1.1.2" }, "pro": true } diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json index f10da74e28..d1d34a8967 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json @@ -17,7 +17,8 @@ "@blocknote/shadcn": "latest", "react": "^19.1.0", "react-dom": "^19.1.0", - "@blocknote/xl-email-exporter": "latest" + "@blocknote/xl-email-exporter": "latest", + "@react-email/render": "^1.1.2" }, "devDependencies": { "@types/react": "^19.1.0", diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/src/App.tsx b/examples/05-interoperability/08-converting-blocks-to-react-email/src/App.tsx index cfd1feacbc..20e11768e2 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/src/App.tsx +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/src/App.tsx @@ -21,11 +21,18 @@ import { ReactEmailExporter, reactEmailDefaultSchemaMappings, } from "@blocknote/xl-email-exporter"; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import "./styles.css"; export default function App() { + // Stores the editor's contents as an email HTML string for download and + // displaying the email. + const [emailDocument, setEmailDocument] = useState(""); + + // Toggles between editor and email view. + const [show, setShow] = useState<"editor" | "email">("editor"); + // Creates a new editor instance. const editor = useCreateBlockNote({ // Adds support for page breaks. @@ -320,21 +327,25 @@ export default function App() { }); // Additional Slash Menu items for page breaks. - const slashMenuItems = useMemo(() => { - return combineByGroup( - getDefaultReactSlashMenuItems(editor), - getPageBreakReactSlashMenuItems(editor), - ); - }, [editor]); + const getSlashMenuItems = useMemo( + () => async (query: string) => + filterSuggestionItems( + combineByGroup( + getDefaultReactSlashMenuItems(editor), + getPageBreakReactSlashMenuItems(editor), + ), + query, + ), + [editor], + ); const existingContext = useBlockNoteContext(); const systemColorScheme = usePrefersColorScheme(); - // Exports the editor content to HTML (for email) and downloads it. - const onDownloadClick = async () => { + // Exports the editor document to email whenever it changes. + const onChange = async () => { const colorScheme = existingContext?.colorSchemePreference || systemColorScheme; - const exporter = new ReactEmailExporter( editor.schema, reactEmailDefaultSchemaMappings, @@ -343,10 +354,18 @@ export default function App() { colorScheme === "dark" ? COLORS_DARK_MODE_DEFAULT : COLORS_DEFAULT, }, ); - const blob = new Blob( - [await exporter.toReactEmailDocument(editor.document)], - { type: "text/html" }, - ); + const emailHtml = await exporter.toReactEmailDocument(editor.document); + setEmailDocument(emailHtml); + }; + + // Exports the inital editor document to PDF. + useEffect(() => { + onChange(); + }, []); + + // Downloads the email in HTML format. + const onDownloadClick = async () => { + const blob = new Blob([emailDocument], { type: "text/html" }); const link = document.createElement("a"); link.href = window.URL.createObjectURL(blob); @@ -363,13 +382,38 @@ export default function App() { window.URL.revokeObjectURL(link.href); }; - // Renders the editor instance, and a download button above. + // Renders the editor instance or PDF view. Also renders two buttons above + // for switching between views and downloading the PDF. return (
- - +
+ + +
+
+ {show === "editor" ? ( + + + + ) : ( +
+ )} +
); } diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/src/styles.css b/examples/05-interoperability/08-converting-blocks-to-react-email/src/styles.css index 9508ea8db3..54a75f0b6c 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/src/styles.css +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/src/styles.css @@ -3,10 +3,24 @@ display: flex; flex-direction: column; gap: 8px; + height: 100%; padding: 8px; width: 100%; } +.download-buttons { + display: flex; + gap: 8px; + width: 100%; +} + +.download-view { + flex: 1; + height: 0; + overflow: auto; + width: 100%; +} + .download-button { background-color: #0090ff; border: none; diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 5bdea13339..49ef038167 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -1108,7 +1108,8 @@ "" ], "dependencies": { - "@blocknote/xl-email-exporter": "latest" + "@blocknote/xl-email-exporter": "latest", + "@react-email/render": "^1.1.2" } as any, "pro": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6a14334d2..b248fe29ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2344,6 +2344,9 @@ importers: '@blocknote/xl-email-exporter': specifier: latest version: link:../../../packages/xl-email-exporter + '@react-email/render': + specifier: ^1.1.2 + version: 1.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.1.0 version: 19.1.0 From 1e0d585abccafc9fa7754b76d7215b81a67a9581 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 12 Sep 2025 16:25:49 +0200 Subject: [PATCH 3/4] Overhauled import/export example styling --- .../01-converting-blocks-to-html/src/App.tsx | 24 ++++--- .../src/styles.css | 63 ++++++++++++++---- .../src/App.tsx | 25 +++---- .../src/styles.css | 65 +++++++++++++++---- .../03-converting-blocks-to-md/src/App.tsx | 24 ++++--- .../03-converting-blocks-to-md/src/styles.css | 63 ++++++++++++++---- .../04-converting-blocks-from-md/src/App.tsx | 31 +++++---- .../src/styles.css | 65 +++++++++++++++---- .../05-converting-blocks-to-pdf/src/App.tsx | 36 +++++----- .../src/styles.css | 64 +++++++++++++----- .../06-converting-blocks-to-docx/src/App.tsx | 29 +++++---- .../src/styles.css | 64 ++++++++++++++---- .../07-converting-blocks-to-odt/src/App.tsx | 29 +++++---- .../src/styles.css | 64 ++++++++++++++---- .../src/App.tsx | 36 +++++----- .../src/styles.css | 64 +++++++++++++----- 16 files changed, 533 insertions(+), 213 deletions(-) diff --git a/examples/05-interoperability/01-converting-blocks-to-html/src/App.tsx b/examples/05-interoperability/01-converting-blocks-to-html/src/App.tsx index 70e8f24ca9..f05df619d6 100644 --- a/examples/05-interoperability/01-converting-blocks-to-html/src/App.tsx +++ b/examples/05-interoperability/01-converting-blocks-to-html/src/App.tsx @@ -41,18 +41,22 @@ export default function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Renders the editor instance, and its contents as HTML below. + // Renders the editor instance and HTML output. return ( -
-
Input (BlockNote Editor):
-
- +
+
+
Editor Input
+
+ +
-
Output (HTML):
-
-
-          {html}
-        
+
+
HTML Output
+
+
+            {html}
+          
+
); diff --git a/examples/05-interoperability/01-converting-blocks-to-html/src/styles.css b/examples/05-interoperability/01-converting-blocks-to-html/src/styles.css index 6d5eeba7fe..4e38a82106 100644 --- a/examples/05-interoperability/01-converting-blocks-to-html/src/styles.css +++ b/examples/05-interoperability/01-converting-blocks-to-html/src/styles.css @@ -1,25 +1,64 @@ -.wrapper { +.views { + container-name: views; + container-type: inline-size; display: flex; - flex-direction: column; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; height: 100%; + padding: 8px; +} + +.view-wrapper { + display: flex; + flex-direction: column; + height: calc(50% - 4px); + width: 100%; +} + +@container views (width > 1024px) { + .view-wrapper { + height: 100%; + width: calc(50% - 4px); + } +} + +.view-label { + color: #0090ff; + display: flex; + font-size: 12px; + font-weight: bold; + justify-content: space-between; + margin-inline: 16px; } -.item { - border-radius: 0.5rem; +.view { + border: solid #0090ff 1px; + border-radius: 16px; flex: 1; - overflow: hidden; + height: 0; + padding: 8px; } -.item.bordered { - border: 1px solid gray; +.view .bn-container { + height: 100%; + margin: 0; + max-width: none; + padding: 0; } -.item pre { - border-radius: 0.5rem; +.view .bn-editor { height: 100%; overflow: auto; - padding-block: 1rem; - padding-inline: 54px; - width: 100%; +} + +.view pre { + background-color: #0090ff20; + border-radius: 8px; + flex: 1; + height: 100%; + margin: 0; + overflow: auto; + padding: 8px; white-space: pre-wrap; } diff --git a/examples/05-interoperability/02-converting-blocks-from-html/src/App.tsx b/examples/05-interoperability/02-converting-blocks-from-html/src/App.tsx index f4533ee19c..8eea7c7ac1 100644 --- a/examples/05-interoperability/02-converting-blocks-from-html/src/App.tsx +++ b/examples/05-interoperability/02-converting-blocks-from-html/src/App.tsx @@ -31,19 +31,22 @@ export default function App() { loadInitialHTML(); }, [editor]); - // Renders a text area for you to write/paste HTML in, and the editor instance - // below, which displays the current HTML as blocks. + // Renders the HTML input and editor instance. return ( -
-
Input (HTML):
-
- -