From 07b0b98ae8f3cac116b358dbc3a764c5034b8f93 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 30 Nov 2023 15:43:42 +0100 Subject: [PATCH 1/3] Added playgrounds for vanilla blocks, React blocks, and vanilla inline content --- examples/editor/examples/Blocks.tsx | 215 ++++++++++++++++++ examples/editor/examples/InlineContent.tsx | 98 ++++++++ examples/editor/examples/ReactBlocks.tsx | 93 ++++++++ .../editor/examples/ReactInlineContent.tsx | 1 - examples/editor/src/main.tsx | 21 +- 5 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 examples/editor/examples/Blocks.tsx create mode 100644 examples/editor/examples/InlineContent.tsx create mode 100644 examples/editor/examples/ReactBlocks.tsx diff --git a/examples/editor/examples/Blocks.tsx b/examples/editor/examples/Blocks.tsx new file mode 100644 index 0000000000..26ce537bca --- /dev/null +++ b/examples/editor/examples/Blocks.tsx @@ -0,0 +1,215 @@ +import { + createBlockSpec, + CustomBlockConfig, + 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 +export 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", + }, +} as const; + +export const alertConfig = { + type: "alert" as const, + propSchema: { + textAlignment: defaultProps.textAlignment, + textColor: defaultProps.textColor, + type: { + default: "warning" as const, + values: ["warning", "error", "info", "success"] as const, + }, + }, + content: "inline", +} satisfies CustomBlockConfig; + +export const alertBlock = createBlockSpec(alertConfig, { + 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"); + Object.entries(inlineContentStyles).forEach(([key, value]) => { + inlineContent.style[key as any] = value; + }); + alert.appendChild(inlineContent); + + return { + dom: alert, + contentDOM: inlineContent, + }; + }, +}); + +export const alertStyles = { + display: "flex", + justifyContent: "center", + alignItems: "center", + flexGrow: "1", + height: "48px", + padding: "4px", + maxWidth: "100%", +} as const; + +export const inlineContentStyles = { + flexGrow: "1", +}; + +export const bracketsParagraphConfig = { + type: "bracketsParagraph", + content: "inline", + propSchema: { + ...defaultProps, + }, +} satisfies CustomBlockConfig; + +export const bracketsParagraphBlock = createBlockSpec(bracketsParagraphConfig, { + 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"); + Object.entries(inlineContentStyles).forEach(([key, value]) => { + inlineContent.style[key as any] = value; + }); + 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, + }; + }, +}); + +export const bracketsParagraphStyles = { + display: "flex", + justifyContent: "center", + alignItems: "center", + flexGrow: "1", + height: "48px", + padding: "4px", + maxWidth: "100%", +} as const; + +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/InlineContent.tsx b/examples/editor/examples/InlineContent.tsx new file mode 100644 index 0000000000..1b47f06db2 --- /dev/null +++ b/examples/editor/examples/InlineContent.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/examples/ReactBlocks.tsx b/examples/editor/examples/ReactBlocks.tsx new file mode 100644 index 0000000000..463b6044b3 --- /dev/null +++ b/examples/editor/examples/ReactBlocks.tsx @@ -0,0 +1,93 @@ +import { defaultBlockSpecs } from "@blocknote/core"; +import "@blocknote/core/style.css"; +import { + BlockNoteView, + createReactBlockSpec, + useBlockNote, +} from "@blocknote/react"; +import { + alertConfig, + alertStyles, + alertTypes, + bracketsParagraphConfig, + bracketsParagraphStyles, + inlineContentStyles, +} from "./Blocks"; + +type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; + +export const alertBlock = createReactBlockSpec(alertConfig, { + render: (props) => ( +
+ +
+
+ ), +}); + +export const bracketsParagraphBlock = createReactBlockSpec( + bracketsParagraphConfig, + { + 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/ReactInlineContent.tsx index 07ec3deb13..2c2bc05add 100644 --- a/examples/editor/examples/ReactInlineContent.tsx +++ b/examples/editor/examples/ReactInlineContent.tsx @@ -75,7 +75,6 @@ export function ReactInlineContent() { "I love ", { type: "tag", - // props: {}, content: "BlockNote", } as any, ], diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index 0d2f29eefe..a6a3cb29dd 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -2,16 +2,20 @@ import { AppShell, Navbar, ScrollArea } from "@mantine/core"; import React from "react"; import { createRoot } from "react-dom/client"; import { + createBrowserRouter, Link, Outlet, RouterProvider, - createBrowserRouter, } from "react-router-dom"; import { App } from "../examples/Basic"; import { ReactInlineContent } from "../examples/ReactInlineContent"; import { ReactStyles } from "../examples/ReactStyles"; +import { ReactCustomBlocks } from "../examples/ReactBlocks"; import "./style.css"; +import { CustomBlocks } from "../examples/Blocks"; +import { InlineContent } from "../examples/InlineContent"; + window.React = React; const editors = [ @@ -25,11 +29,26 @@ const editors = [ path: "/react-styles", component: ReactStyles, }, + { + title: "Inline content", + path: "/inline-content", + component: InlineContent, + }, { title: "React inline content", path: "/react-inline-content", component: ReactInlineContent, }, + { + title: "Custom blocks", + path: "/custom-blocks", + component: CustomBlocks, + }, + { + title: "React custom blocks", + path: "/react-blocks", + component: ReactCustomBlocks, + }, ]; function Root() { From ed9971bcf9f7f8d8ed98b18fcf756f0aa0196e72 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 30 Nov 2023 19:37:18 +0100 Subject: [PATCH 2/3] fix renderHTML error --- .../src/extensions/Blocks/api/blocks/createSpec.ts | 11 +++++++++++ packages/react/src/ReactBlockSpec.tsx | 11 +++++++++++ 2 files changed, 22 insertions(+) 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( From f25bcb690ddabb4b6845ef9d32897dda1a955596 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 30 Nov 2023 19:37:22 +0100 Subject: [PATCH 3/3] clean examples --- examples/editor/examples/Blocks.tsx | 215 ------------------ examples/editor/examples/ReactBlocks.tsx | 93 -------- .../examples/{Basic.tsx => basic/App.tsx} | 0 .../App.tsx} | 0 .../examples/react-custom-blocks/App.tsx | 143 ++++++++++++ .../App.tsx} | 0 .../App.tsx} | 0 .../examples/vanilla-custom-blocks/App.tsx | 213 +++++++++++++++++ .../App.tsx} | 0 examples/editor/src/main.tsx | 18 +- 10 files changed, 365 insertions(+), 317 deletions(-) delete mode 100644 examples/editor/examples/Blocks.tsx delete mode 100644 examples/editor/examples/ReactBlocks.tsx rename examples/editor/examples/{Basic.tsx => basic/App.tsx} (100%) rename examples/editor/examples/{Collaboration.tsx => collaboration/App.tsx} (100%) create mode 100644 examples/editor/examples/react-custom-blocks/App.tsx rename examples/editor/examples/{ReactInlineContent.tsx => react-custom-inline-content/App.tsx} (100%) rename examples/editor/examples/{ReactStyles.tsx => react-custom-styles/App.tsx} (100%) create mode 100644 examples/editor/examples/vanilla-custom-blocks/App.tsx rename examples/editor/examples/{InlineContent.tsx => vanilla-custom-inline-content/App.tsx} (100%) diff --git a/examples/editor/examples/Blocks.tsx b/examples/editor/examples/Blocks.tsx deleted file mode 100644 index 26ce537bca..0000000000 --- a/examples/editor/examples/Blocks.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { - createBlockSpec, - CustomBlockConfig, - 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 -export 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", - }, -} as const; - -export const alertConfig = { - type: "alert" as const, - propSchema: { - textAlignment: defaultProps.textAlignment, - textColor: defaultProps.textColor, - type: { - default: "warning" as const, - values: ["warning", "error", "info", "success"] as const, - }, - }, - content: "inline", -} satisfies CustomBlockConfig; - -export const alertBlock = createBlockSpec(alertConfig, { - 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"); - Object.entries(inlineContentStyles).forEach(([key, value]) => { - inlineContent.style[key as any] = value; - }); - alert.appendChild(inlineContent); - - return { - dom: alert, - contentDOM: inlineContent, - }; - }, -}); - -export const alertStyles = { - display: "flex", - justifyContent: "center", - alignItems: "center", - flexGrow: "1", - height: "48px", - padding: "4px", - maxWidth: "100%", -} as const; - -export const inlineContentStyles = { - flexGrow: "1", -}; - -export const bracketsParagraphConfig = { - type: "bracketsParagraph", - content: "inline", - propSchema: { - ...defaultProps, - }, -} satisfies CustomBlockConfig; - -export const bracketsParagraphBlock = createBlockSpec(bracketsParagraphConfig, { - 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"); - Object.entries(inlineContentStyles).forEach(([key, value]) => { - inlineContent.style[key as any] = value; - }); - 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, - }; - }, -}); - -export const bracketsParagraphStyles = { - display: "flex", - justifyContent: "center", - alignItems: "center", - flexGrow: "1", - height: "48px", - padding: "4px", - maxWidth: "100%", -} as const; - -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/ReactBlocks.tsx b/examples/editor/examples/ReactBlocks.tsx deleted file mode 100644 index 463b6044b3..0000000000 --- a/examples/editor/examples/ReactBlocks.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { defaultBlockSpecs } from "@blocknote/core"; -import "@blocknote/core/style.css"; -import { - BlockNoteView, - createReactBlockSpec, - useBlockNote, -} from "@blocknote/react"; -import { - alertConfig, - alertStyles, - alertTypes, - bracketsParagraphConfig, - bracketsParagraphStyles, - inlineContentStyles, -} from "./Blocks"; - -type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; - -export const alertBlock = createReactBlockSpec(alertConfig, { - render: (props) => ( -
- -
-
- ), -}); - -export const bracketsParagraphBlock = createReactBlockSpec( - bracketsParagraphConfig, - { - 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/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 100% rename from examples/editor/examples/ReactInlineContent.tsx rename to examples/editor/examples/react-custom-inline-content/App.tsx 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/InlineContent.tsx b/examples/editor/examples/vanilla-custom-inline-content/App.tsx similarity index 100% rename from examples/editor/examples/InlineContent.tsx rename to examples/editor/examples/vanilla-custom-inline-content/App.tsx diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index a6a3cb29dd..3d13ac7755 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -2,19 +2,19 @@ import { AppShell, Navbar, ScrollArea } from "@mantine/core"; import React from "react"; import { createRoot } from "react-dom/client"; import { - createBrowserRouter, Link, Outlet, RouterProvider, + createBrowserRouter, } from "react-router-dom"; -import { App } from "../examples/Basic"; -import { ReactInlineContent } from "../examples/ReactInlineContent"; -import { ReactStyles } from "../examples/ReactStyles"; -import { ReactCustomBlocks } from "../examples/ReactBlocks"; +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"; -import { CustomBlocks } from "../examples/Blocks"; -import { InlineContent } from "../examples/InlineContent"; window.React = React; @@ -30,7 +30,7 @@ const editors = [ component: ReactStyles, }, { - title: "Inline content", + title: "Vanilla Inline content", path: "/inline-content", component: InlineContent, }, @@ -40,7 +40,7 @@ const editors = [ component: ReactInlineContent, }, { - title: "Custom blocks", + title: "Vanilla custom blocks", path: "/custom-blocks", component: CustomBlocks, },