Skip to content

Commit 8ef3770

Browse files
fix: Visual differences between live editor and rendered exported HTML (#2348)
* Fixed visual differences between live editor and rendered exported HTML * Updated screenshots * Fixed unit tests & added static version of default blocks example * Added e2e test * Implemented PR feedback * Implemented PR feedback
1 parent 5160d73 commit 8ef3770

41 files changed

Lines changed: 1431 additions & 317 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"playground": true,
3+
"docs": false,
4+
"author": "matthewlipski",
5+
"tags": ["Basic", "Blocks", "Import/Export"]
6+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Rendering HTML Converted From Blocks (Side by Side)
2+
3+
This example exports the current document (all blocks) as HTML and renders it below the editor.
4+
5+
**Try it out:** Edit the document to see the rendered static HTML!
6+
7+
**Relevant Docs:**
8+
9+
- [Converting Blocks to HTML](/docs/features/export/html)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<html lang="en">
2+
<head>
3+
<meta charset="UTF-8" />
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
5+
<title>Rendering HTML Converted From Blocks (Side by Side)</title>
6+
<script>
7+
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
8+
</script>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="./main.tsx"></script>
13+
</body>
14+
</html>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
2+
import React from "react";
3+
import { createRoot } from "react-dom/client";
4+
import App from "./src/App.jsx";
5+
6+
const root = createRoot(document.getElementById("root")!);
7+
root.render(
8+
<React.StrictMode>
9+
<App />
10+
</React.StrictMode>
11+
);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@blocknote/example-interoperability-blocks-to-html-static-render",
3+
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
4+
"type": "module",
5+
"private": true,
6+
"version": "0.12.4",
7+
"scripts": {
8+
"start": "vite",
9+
"dev": "vite",
10+
"build:prod": "tsc && vite build",
11+
"preview": "vite preview"
12+
},
13+
"dependencies": {
14+
"@blocknote/ariakit": "latest",
15+
"@blocknote/core": "latest",
16+
"@blocknote/mantine": "latest",
17+
"@blocknote/react": "latest",
18+
"@blocknote/shadcn": "latest",
19+
"@mantine/core": "^8.3.11",
20+
"@mantine/hooks": "^8.3.11",
21+
"@mantine/utils": "^6.0.22",
22+
"react": "^19.2.1",
23+
"react-dom": "^19.2.1"
24+
},
25+
"devDependencies": {
26+
"@types/react": "^19.2.2",
27+
"@types/react-dom": "^19.2.2",
28+
"@vitejs/plugin-react": "^4.7.0",
29+
"vite": "^5.4.20"
30+
}
31+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import "@blocknote/core/fonts/inter.css";
2+
import { BlockNoteView } from "@blocknote/mantine";
3+
import "@blocknote/mantine/style.css";
4+
import { useCreateBlockNote, usePrefersColorScheme } from "@blocknote/react";
5+
import { useCallback, useEffect, useRef, useState } from "react";
6+
7+
import "./styles.css";
8+
9+
export default function App() {
10+
// Creates a new editor instance with some initial content.
11+
const editor = useCreateBlockNote({
12+
initialContent: [
13+
{
14+
type: "paragraph",
15+
content: "Welcome to this demo!",
16+
},
17+
{
18+
type: "paragraph",
19+
},
20+
{
21+
type: "paragraph",
22+
content: [
23+
{
24+
type: "text",
25+
text: "Blocks:",
26+
styles: { bold: true },
27+
},
28+
],
29+
},
30+
{
31+
type: "paragraph",
32+
content: "Paragraph",
33+
},
34+
{
35+
type: "heading",
36+
content: "Heading",
37+
},
38+
{
39+
id: "toggle-heading",
40+
type: "heading",
41+
props: { isToggleable: true },
42+
content: "Toggle Heading",
43+
},
44+
{
45+
type: "quote",
46+
content: "Quote",
47+
},
48+
{
49+
type: "bulletListItem",
50+
content: "Bullet List Item",
51+
},
52+
{
53+
type: "numberedListItem",
54+
content: "Numbered List Item",
55+
},
56+
{
57+
type: "checkListItem",
58+
content: "Check List Item",
59+
},
60+
{
61+
id: "toggle-list-item",
62+
type: "toggleListItem",
63+
content: "Toggle List Item",
64+
},
65+
{
66+
type: "codeBlock",
67+
props: { language: "javascript" },
68+
content: "console.log('Hello, world!');",
69+
},
70+
{
71+
type: "table",
72+
content: {
73+
type: "tableContent",
74+
rows: [
75+
{
76+
cells: ["Table Cell", "Table Cell", "Table Cell"],
77+
},
78+
{
79+
cells: ["Table Cell", "Table Cell", "Table Cell"],
80+
},
81+
{
82+
cells: ["Table Cell", "Table Cell", "Table Cell"],
83+
},
84+
],
85+
},
86+
},
87+
{
88+
type: "file",
89+
},
90+
{
91+
type: "image",
92+
props: {
93+
url: "https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
94+
caption:
95+
"From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
96+
},
97+
},
98+
{
99+
type: "video",
100+
props: {
101+
url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
102+
caption:
103+
"From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
104+
},
105+
},
106+
{
107+
type: "audio",
108+
props: {
109+
url: "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
110+
caption:
111+
"From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
112+
},
113+
},
114+
{
115+
type: "paragraph",
116+
},
117+
{
118+
type: "paragraph",
119+
content: [
120+
{
121+
type: "text",
122+
text: "Inline Content:",
123+
styles: { bold: true },
124+
},
125+
],
126+
},
127+
{
128+
type: "paragraph",
129+
content: [
130+
{
131+
type: "text",
132+
text: "Styled Text",
133+
styles: {
134+
bold: true,
135+
italic: true,
136+
textColor: "red",
137+
backgroundColor: "blue",
138+
},
139+
},
140+
{
141+
type: "text",
142+
text: " ",
143+
styles: {},
144+
},
145+
{
146+
type: "link",
147+
content: "Link",
148+
href: "https://www.blocknotejs.org",
149+
},
150+
],
151+
},
152+
{
153+
type: "paragraph",
154+
},
155+
],
156+
});
157+
158+
const [html, setHTML] = useState("");
159+
160+
const ref = useRef<HTMLDivElement>(null);
161+
const systemColorScheme = usePrefersColorScheme();
162+
const theme =
163+
systemColorScheme === "no-preference" ? "light" : systemColorScheme;
164+
165+
// Function to update the rendered static HTML.
166+
const updateRenderedHTML = useCallback(async () => {
167+
setHTML(editor.blocksToFullHTML(editor.document));
168+
}, []);
169+
// Updates rendered static HTML with initial editor content.
170+
useEffect(() => {
171+
updateRenderedHTML();
172+
}, []);
173+
174+
// Renders the editor instance and HTML output.
175+
return (
176+
<div className="views">
177+
<div className="view-wrapper">
178+
<div className="view-label">Editor Input</div>
179+
<div className="view">
180+
<BlockNoteView editor={editor} onChange={updateRenderedHTML} />
181+
</div>
182+
</div>
183+
<div className="view-wrapper">
184+
<div className="view-label">Rendered Static HTML Output</div>
185+
<div className="view">
186+
{/* To make the static HTML look identical to the editor, we need to
187+
add these two wrapping divs to the exported blocks. These mock the
188+
wrapping elements of a BlockNote editor, and are needed as the
189+
exported HTML only holds the contents of the editor. You need will
190+
need to add additional class names/attributes depend on the UI
191+
library you're using, whether you want to show light or dark mode,
192+
etc. It's easiest to just copy the class names and HTML attributes
193+
from an actual BlockNote editor. */}
194+
<div
195+
className="bn-container bn-mantine"
196+
data-color-scheme={theme}
197+
data-mantine-color-scheme={theme}
198+
>
199+
<div
200+
className="ProseMirror bn-editor bn-default-styles"
201+
dangerouslySetInnerHTML={{ __html: html }}
202+
/>
203+
</div>
204+
</div>
205+
</div>
206+
</div>
207+
);
208+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
.views {
2+
container-name: views;
3+
container-type: inline-size;
4+
display: flex;
5+
flex-direction: row;
6+
flex-wrap: wrap;
7+
gap: 8px;
8+
height: 100%;
9+
padding: 8px;
10+
}
11+
12+
.view-wrapper {
13+
display: flex;
14+
flex-direction: column;
15+
height: calc(50% - 4px);
16+
width: 100%;
17+
}
18+
19+
@container views (width > 1024px) {
20+
.view-wrapper {
21+
height: 100%;
22+
width: calc(50% - 4px);
23+
}
24+
}
25+
26+
.view-label {
27+
color: #0090ff;
28+
display: flex;
29+
font-size: 12px;
30+
font-weight: bold;
31+
justify-content: space-between;
32+
margin-inline: 16px;
33+
}
34+
35+
.view {
36+
border: solid #0090ff 1px;
37+
border-radius: 16px;
38+
flex: 1;
39+
height: 0;
40+
padding: 8px;
41+
}
42+
43+
.view .bn-container {
44+
height: 100%;
45+
margin: 0;
46+
max-width: none;
47+
padding: 0;
48+
}
49+
50+
.view .bn-editor {
51+
height: 100%;
52+
overflow: auto;
53+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
3+
"compilerOptions": {
4+
"target": "ESNext",
5+
"useDefineForClassFields": true,
6+
"lib": [
7+
"DOM",
8+
"DOM.Iterable",
9+
"ESNext"
10+
],
11+
"allowJs": false,
12+
"skipLibCheck": true,
13+
"esModuleInterop": false,
14+
"allowSyntheticDefaultImports": true,
15+
"strict": true,
16+
"forceConsistentCasingInFileNames": true,
17+
"module": "ESNext",
18+
"moduleResolution": "bundler",
19+
"resolveJsonModule": true,
20+
"isolatedModules": true,
21+
"noEmit": true,
22+
"jsx": "react-jsx",
23+
"composite": true
24+
},
25+
"include": [
26+
"."
27+
],
28+
"__ADD_FOR_LOCAL_DEV_references": [
29+
{
30+
"path": "../../../packages/core/"
31+
},
32+
{
33+
"path": "../../../packages/react/"
34+
}
35+
]
36+
}

0 commit comments

Comments
 (0)