diff --git a/examples/editor/examples/Basic.tsx b/examples/editor/examples/basic/App.tsx
similarity index 100%
rename from examples/editor/examples/Basic.tsx
rename to examples/editor/examples/basic/App.tsx
diff --git a/examples/editor/examples/Collaboration.tsx b/examples/editor/examples/collaboration/App.tsx
similarity index 100%
rename from examples/editor/examples/Collaboration.tsx
rename to examples/editor/examples/collaboration/App.tsx
diff --git a/examples/editor/examples/react-custom-blocks/App.tsx b/examples/editor/examples/react-custom-blocks/App.tsx
new file mode 100644
index 0000000000..43750998a0
--- /dev/null
+++ b/examples/editor/examples/react-custom-blocks/App.tsx
@@ -0,0 +1,143 @@
+import { defaultBlockSpecs, defaultProps } from "@blocknote/core";
+import "@blocknote/core/style.css";
+import {
+ BlockNoteView,
+ createReactBlockSpec,
+ useBlockNote,
+} from "@blocknote/react";
+
+type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any };
+
+// The types of alerts that users can choose from
+const alertTypes = {
+ warning: {
+ icon: "⚠️",
+ color: "#e69819",
+ backgroundColor: "#fff6e6",
+ },
+ error: {
+ icon: "⛔",
+ color: "#d80d0d",
+ backgroundColor: "#ffe6e6",
+ },
+ info: {
+ icon: "ℹ️",
+ color: "#507aff",
+ backgroundColor: "#e6ebff",
+ },
+ success: {
+ icon: "✅",
+ color: "#0bc10b",
+ backgroundColor: "#e6ffe6",
+ },
+};
+
+export const alertBlock = createReactBlockSpec(
+ {
+ type: "alert",
+ propSchema: {
+ textAlignment: defaultProps.textAlignment,
+ textColor: defaultProps.textColor,
+ type: {
+ default: "warning" as const,
+ values: ["warning", "error", "info", "success"] as const,
+ },
+ },
+ content: "inline",
+ },
+ {
+ render: (props) => (
+
+
+
+
+ ),
+ }
+);
+
+export const bracketsParagraphBlock = createReactBlockSpec(
+ {
+ type: "bracketsParagraph",
+ content: "inline",
+ propSchema: {
+ ...defaultProps,
+ },
+ },
+ {
+ render: (props) => (
+
+
{"["}
+
{"{"}
+
+
{"}"}
+
{"]"}
+
+ ),
+ }
+);
+
+export function ReactCustomBlocks() {
+ const editor = useBlockNote({
+ domAttributes: {
+ editor: {
+ class: "editor",
+ "data-test": "editor",
+ },
+ },
+ blockSpecs: {
+ ...defaultBlockSpecs,
+ alert: alertBlock,
+ bracketsParagraph: bracketsParagraphBlock,
+ },
+ initialContent: [
+ {
+ type: "alert",
+ props: {
+ type: "success",
+ },
+ content: "Alert",
+ },
+ {
+ type: "bracketsParagraph",
+ content: "Brackets Paragraph",
+ },
+ ],
+ });
+
+ // Give tests a way to get prosemirror instance
+ (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor;
+
+ return ;
+}
diff --git a/examples/editor/examples/ReactInlineContent.tsx b/examples/editor/examples/react-custom-inline-content/App.tsx
similarity index 98%
rename from examples/editor/examples/ReactInlineContent.tsx
rename to examples/editor/examples/react-custom-inline-content/App.tsx
index 07ec3deb13..2c2bc05add 100644
--- a/examples/editor/examples/ReactInlineContent.tsx
+++ b/examples/editor/examples/react-custom-inline-content/App.tsx
@@ -75,7 +75,6 @@ export function ReactInlineContent() {
"I love ",
{
type: "tag",
- // props: {},
content: "BlockNote",
} as any,
],
diff --git a/examples/editor/examples/ReactStyles.tsx b/examples/editor/examples/react-custom-styles/App.tsx
similarity index 100%
rename from examples/editor/examples/ReactStyles.tsx
rename to examples/editor/examples/react-custom-styles/App.tsx
diff --git a/examples/editor/examples/vanilla-custom-blocks/App.tsx b/examples/editor/examples/vanilla-custom-blocks/App.tsx
new file mode 100644
index 0000000000..2bdfde7ce9
--- /dev/null
+++ b/examples/editor/examples/vanilla-custom-blocks/App.tsx
@@ -0,0 +1,213 @@
+import {
+ createBlockSpec,
+ defaultBlockSpecs,
+ defaultProps,
+} from "@blocknote/core";
+import "@blocknote/core/style.css";
+import { BlockNoteView, useBlockNote } from "@blocknote/react";
+
+type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any };
+
+// The types of alerts that users can choose from
+const alertTypes = {
+ warning: {
+ icon: "⚠️",
+ color: "#e69819",
+ backgroundColor: "#fff6e6",
+ },
+ error: {
+ icon: "⛔",
+ color: "#d80d0d",
+ backgroundColor: "#ffe6e6",
+ },
+ info: {
+ icon: "ℹ️",
+ color: "#507aff",
+ backgroundColor: "#e6ebff",
+ },
+ success: {
+ icon: "✅",
+ color: "#0bc10b",
+ backgroundColor: "#e6ffe6",
+ },
+};
+
+const alertBlock = createBlockSpec(
+ {
+ type: "alert",
+ propSchema: {
+ textAlignment: defaultProps.textAlignment,
+ textColor: defaultProps.textColor,
+ type: {
+ default: "warning" as const,
+ values: ["warning", "error", "info", "success"] as const,
+ },
+ },
+ content: "inline",
+ },
+ {
+ render: (block, editor) => {
+ const alert = document.createElement("div");
+ Object.entries(alertStyles).forEach(([key, value]) => {
+ alert.style[key as any] = value;
+ });
+ alert.style.backgroundColor =
+ alertTypes[block.props.type].backgroundColor;
+
+ const dropdown = document.createElement("select");
+ dropdown.contentEditable = "false";
+ dropdown.addEventListener("change", () => {
+ // TODO: Something is not quite right with the typing seems like
+ editor.updateBlock(block, {
+ type: "alert",
+ props: { type: dropdown.value as keyof typeof alertTypes },
+ });
+ });
+ dropdown.options.add(
+ new Option(
+ alertTypes["warning"].icon,
+ "warning",
+ block.props.type === "warning",
+ block.props.type === "warning"
+ )
+ );
+ dropdown.options.add(
+ new Option(
+ alertTypes["error"].icon,
+ "error",
+ block.props.type === "error",
+ block.props.type === "error"
+ )
+ );
+ dropdown.options.add(
+ new Option(
+ alertTypes["info"].icon,
+ "info",
+ block.props.type === "info",
+ block.props.type === "info"
+ )
+ );
+ dropdown.options.add(
+ new Option(
+ alertTypes["success"].icon,
+ "success",
+ block.props.type === "success",
+ block.props.type === "success"
+ )
+ );
+ alert.appendChild(dropdown);
+
+ const inlineContent = document.createElement("div");
+ inlineContent.style.flexGrow = "1";
+
+ alert.appendChild(inlineContent);
+
+ return {
+ dom: alert,
+ contentDOM: inlineContent,
+ };
+ },
+ }
+);
+
+// TODO: use CSS?
+const alertStyles = {
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ flexGrow: "1",
+ height: "48px",
+ padding: "4px",
+ maxWidth: "100%",
+};
+
+const bracketsParagraphBlock = createBlockSpec(
+ {
+ type: "bracketsParagraph",
+ content: "inline",
+ propSchema: {
+ ...defaultProps,
+ },
+ },
+ {
+ render: () => {
+ const bracketsParagraph = document.createElement("div");
+ Object.entries(bracketsParagraphStyles).forEach(([key, value]) => {
+ bracketsParagraph.style[key as any] = value;
+ });
+
+ const leftBracket = document.createElement("div");
+ leftBracket.contentEditable = "false";
+ leftBracket.innerText = "[";
+ bracketsParagraph.appendChild(leftBracket);
+ const leftCurlyBracket = document.createElement("span");
+ leftCurlyBracket.contentEditable = "false";
+ leftCurlyBracket.innerText = "{";
+ bracketsParagraph.appendChild(leftCurlyBracket);
+
+ const inlineContent = document.createElement("div");
+ inlineContent.style.flexGrow = "1";
+
+ bracketsParagraph.appendChild(inlineContent);
+
+ const rightCurlyBracket = document.createElement("span");
+ rightCurlyBracket.contentEditable = "false";
+ rightCurlyBracket.innerText = "}";
+ bracketsParagraph.appendChild(rightCurlyBracket);
+ const rightBracket = document.createElement("div");
+ rightBracket.contentEditable = "false";
+ rightBracket.innerText = "]";
+ bracketsParagraph.appendChild(rightBracket);
+
+ return {
+ dom: bracketsParagraph,
+ contentDOM: inlineContent,
+ };
+ },
+ }
+);
+
+// TODO: use CSS
+const bracketsParagraphStyles = {
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ flexGrow: "1",
+ height: "48px",
+ padding: "4px",
+ maxWidth: "100%",
+};
+
+export function CustomBlocks() {
+ const editor = useBlockNote({
+ domAttributes: {
+ editor: {
+ class: "editor",
+ "data-test": "editor",
+ },
+ },
+ blockSpecs: {
+ ...defaultBlockSpecs,
+ alert: alertBlock,
+ bracketsParagraph: bracketsParagraphBlock,
+ },
+ initialContent: [
+ {
+ type: "alert",
+ props: {
+ type: "success",
+ },
+ content: ["Alert"],
+ },
+ {
+ type: "bracketsParagraph",
+ content: "Brackets Paragraph",
+ },
+ ],
+ });
+
+ // Give tests a way to get prosemirror instance
+ (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor;
+
+ return ;
+}
diff --git a/examples/editor/examples/vanilla-custom-inline-content/App.tsx b/examples/editor/examples/vanilla-custom-inline-content/App.tsx
new file mode 100644
index 0000000000..1b47f06db2
--- /dev/null
+++ b/examples/editor/examples/vanilla-custom-inline-content/App.tsx
@@ -0,0 +1,98 @@
+import {
+ createInlineContentSpec,
+ defaultInlineContentSpecs,
+} from "@blocknote/core";
+import "@blocknote/core/style.css";
+import { BlockNoteView, useBlockNote } from "@blocknote/react";
+
+type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any };
+
+const mention = createInlineContentSpec(
+ {
+ type: "mention",
+ propSchema: {
+ user: {
+ default: "",
+ },
+ },
+ content: "none",
+ },
+ {
+ render: (inlineContent) => {
+ const mention = document.createElement("span");
+ mention.textContent = `@${inlineContent.props.user}`;
+
+ return {
+ dom: mention,
+ };
+ },
+ }
+);
+
+const tag = createInlineContentSpec(
+ {
+ type: "tag",
+ propSchema: {},
+ content: "styled",
+ },
+ {
+ render: () => {
+ const tag = document.createElement("span");
+ tag.textContent = "#";
+
+ const content = document.createElement("span");
+ tag.appendChild(content);
+
+ return {
+ dom: tag,
+ contentDOM: content,
+ };
+ },
+ }
+);
+
+export function InlineContent() {
+ const editor = useBlockNote({
+ inlineContentSpecs: {
+ mention,
+ tag,
+ ...defaultInlineContentSpecs,
+ },
+ domAttributes: {
+ editor: {
+ class: "editor",
+ "data-test": "editor",
+ },
+ },
+ initialContent: [
+ {
+ type: "paragraph",
+ content: [
+ "I enjoy working with ",
+ {
+ type: "mention",
+ props: {
+ user: "Matthew",
+ },
+ content: undefined,
+ } as any,
+ ],
+ },
+ {
+ type: "paragraph",
+ content: [
+ "I love ",
+ {
+ type: "tag",
+ content: "BlockNote",
+ } as any,
+ ],
+ },
+ ],
+ });
+
+ // Give tests a way to get prosemirror instance
+ (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor;
+
+ return ;
+}
diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx
index 0d2f29eefe..3d13ac7755 100644
--- a/examples/editor/src/main.tsx
+++ b/examples/editor/src/main.tsx
@@ -8,10 +8,14 @@ import {
createBrowserRouter,
} from "react-router-dom";
-import { App } from "../examples/Basic";
-import { ReactInlineContent } from "../examples/ReactInlineContent";
-import { ReactStyles } from "../examples/ReactStyles";
+import { App } from "../examples/basic/App";
+import { ReactCustomBlocks } from "../examples/react-custom-blocks/App";
+import { ReactInlineContent } from "../examples/react-custom-inline-content/App";
+import { ReactStyles } from "../examples/react-custom-styles/App";
+import { CustomBlocks } from "../examples/vanilla-custom-blocks/App";
+import { InlineContent } from "../examples/vanilla-custom-inline-content/App";
import "./style.css";
+
window.React = React;
const editors = [
@@ -25,11 +29,26 @@ const editors = [
path: "/react-styles",
component: ReactStyles,
},
+ {
+ title: "Vanilla Inline content",
+ path: "/inline-content",
+ component: InlineContent,
+ },
{
title: "React inline content",
path: "/react-inline-content",
component: ReactInlineContent,
},
+ {
+ title: "Vanilla custom blocks",
+ path: "/custom-blocks",
+ component: CustomBlocks,
+ },
+ {
+ title: "React custom blocks",
+ path: "/react-blocks",
+ component: ReactCustomBlocks,
+ },
];
function Root() {
diff --git a/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts b/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts
index 1ae2006ff7..de292f228a 100644
--- a/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts
+++ b/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts
@@ -135,6 +135,17 @@ export function createBlockSpec<
return getParseRules(blockConfig, blockImplementation.parse);
},
+ renderHTML() {
+ // renderHTML is not really used, as we always use a nodeView, and we use toExternalHTML / toInternalHTML for serialization
+ // There's an edge case when this gets called nevertheless; before the nodeviews have been mounted
+ // this is why we implement it with a temporary placeholder
+ const div = document.createElement("div");
+ div.setAttribute("data-tmp-placeholder", "true");
+ return {
+ dom: div,
+ };
+ },
+
addNodeView() {
return ({ getPos }) => {
// Gets the BlockNote editor instance
diff --git a/packages/react/src/ReactBlockSpec.tsx b/packages/react/src/ReactBlockSpec.tsx
index 81d1f72d79..634c707eb7 100644
--- a/packages/react/src/ReactBlockSpec.tsx
+++ b/packages/react/src/ReactBlockSpec.tsx
@@ -122,6 +122,17 @@ export function createReactBlockSpec<
return getParseRules(blockConfig, blockImplementation.parse);
},
+ renderHTML() {
+ // renderHTML is not really used, as we always use a nodeView, and we use toExternalHTML / toInternalHTML for serialization
+ // There's an edge case when this gets called nevertheless; before the nodeviews have been mounted
+ // this is why we implement it with a temporary placeholder
+ const div = document.createElement("div");
+ div.setAttribute("data-tmp-placeholder", "true");
+ return {
+ dom: div,
+ };
+ },
+
addNodeView() {
return (props) =>
ReactNodeViewRenderer(