diff --git a/examples/01-basic/04-all-blocks/App.tsx b/examples/01-basic/04-all-blocks/App.tsx
index 5ae6d024f..6a34751b7 100644
--- a/examples/01-basic/04-all-blocks/App.tsx
+++ b/examples/01-basic/04-all-blocks/App.tsx
@@ -1,203 +1,208 @@
import {
- BlockNoteEditorOptions,
BlockNoteSchema,
+ combineByGroup,
+ filterSuggestionItems,
locales,
} from "@blocknote/core";
import "@blocknote/core/fonts/inter.css";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
-import { useCreateBlockNote } from "@blocknote/react";
import {
+ SuggestionMenuController,
+ getDefaultReactSlashMenuItems,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import {
+ getMultiColumnSlashMenuItems,
multiColumnDropCursor,
locales as multiColumnLocales,
withMultiColumn,
} from "@blocknote/xl-multi-column";
+import { useMemo } from "react";
-const schema = withMultiColumn(BlockNoteSchema.create());
-const options = {
- schema: withMultiColumn(BlockNoteSchema.create()),
- dropCursor: multiColumnDropCursor,
- dictionary: {
- ...locales.en,
- multi_column: multiColumnLocales.en,
- },
- initialContent: [
- {
- type: "paragraph",
- content: "Welcome to this demo!",
- },
- {
- type: "paragraph",
- },
- {
- type: "paragraph",
- content: [
- {
- type: "text",
- text: "Blocks:",
- styles: { bold: true },
- },
- ],
- },
- {
- type: "paragraph",
- content: "Paragraph",
- },
- {
- type: "columnList",
- children: [
- {
- type: "column",
- props: {
- width: 0.8,
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ schema: withMultiColumn(BlockNoteSchema.create()),
+ dropCursor: multiColumnDropCursor,
+ dictionary: {
+ ...locales.en,
+ multi_column: multiColumnLocales.en,
+ },
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ },
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "Blocks:",
+ styles: { bold: true },
},
- children: [
- {
- type: "paragraph",
- content: "Hello to the left!",
+ ],
+ },
+ {
+ type: "paragraph",
+ content: "Paragraph",
+ },
+ {
+ type: "columnList",
+ children: [
+ {
+ type: "column",
+ props: {
+ width: 0.8,
},
- ],
- },
- {
- type: "column",
- props: {
- width: 1.2,
+ children: [
+ {
+ type: "paragraph",
+ content: "Hello to the left!",
+ },
+ ],
},
- children: [
+ {
+ type: "column",
+ props: {
+ width: 1.2,
+ },
+ children: [
+ {
+ type: "paragraph",
+ content: "Hello to the right!",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: "heading",
+ content: "Heading",
+ },
+ {
+ type: "bulletListItem",
+ content: "Bullet List Item",
+ },
+ {
+ type: "numberedListItem",
+ content: "Numbered List Item",
+ },
+ {
+ type: "checkListItem",
+ content: "Check List Item",
+ },
+ {
+ type: "codeBlock",
+ props: { language: "javascript" },
+ content: "console.log('Hello, world!');",
+ },
+ {
+ type: "table",
+ content: {
+ type: "tableContent",
+ rows: [
+ {
+ cells: ["Table Cell", "Table Cell", "Table Cell"],
+ },
+ {
+ cells: ["Table Cell", "Table Cell", "Table Cell"],
+ },
{
- type: "paragraph",
- content: "Hello to the right!",
+ cells: ["Table Cell", "Table Cell", "Table Cell"],
},
],
},
- ],
- },
- {
- type: "heading",
- content: "Heading",
- },
- {
- type: "bulletListItem",
- content: "Bullet List Item",
- },
- {
- type: "numberedListItem",
- content: "Numbered List Item",
- },
- {
- type: "checkListItem",
- content: "Check List Item",
- },
- {
- type: "codeBlock",
- props: { language: "javascript" },
- content: "console.log('Hello, world!');",
- },
- {
- type: "table",
- content: {
- type: "tableContent",
- rows: [
+ },
+ {
+ type: "file",
+ },
+ {
+ type: "image",
+ props: {
+ url: "https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+ caption:
+ "From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+ },
+ },
+ {
+ type: "video",
+ props: {
+ url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+ caption:
+ "From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+ },
+ },
+ {
+ type: "audio",
+ props: {
+ url: "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
+ caption:
+ "From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
+ },
+ },
+ {
+ type: "paragraph",
+ },
+ {
+ type: "paragraph",
+ content: [
{
- cells: ["Table Cell", "Table Cell", "Table Cell"],
+ type: "text",
+ text: "Inline Content:",
+ styles: { bold: true },
},
+ ],
+ },
+ {
+ type: "paragraph",
+ content: [
{
- cells: ["Table Cell", "Table Cell", "Table Cell"],
+ type: "text",
+ text: "Styled Text",
+ styles: {
+ bold: true,
+ italic: true,
+ textColor: "red",
+ backgroundColor: "blue",
+ },
+ },
+ {
+ type: "text",
+ text: " ",
+ styles: {},
},
{
- cells: ["Table Cell", "Table Cell", "Table Cell"],
+ type: "link",
+ content: "Link",
+ href: "https://www.blocknotejs.org",
},
],
},
- },
- {
- type: "file",
- },
- {
- type: "image",
- props: {
- url: "https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
- caption:
- "From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
- },
- },
- {
- type: "video",
- props: {
- url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
- caption:
- "From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
- },
- },
- {
- type: "audio",
- props: {
- url: "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
- caption:
- "From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
+ {
+ type: "paragraph",
},
- },
- {
- type: "paragraph",
- },
- {
- type: "paragraph",
- content: [
- {
- type: "text",
- text: "Inline Content:",
- styles: { bold: true },
- },
- ],
- },
- {
- type: "paragraph",
- content: [
- {
- type: "text",
- text: "Styled Text",
- styles: {
- bold: true,
- italic: true,
- textColor: "red",
- backgroundColor: "blue",
- },
- },
- {
- type: "text",
- text: " ",
- styles: {},
- },
- {
- type: "link",
- content: "Link",
- href: "https://www.blocknotejs.org",
- },
- ],
- },
- {
- type: "paragraph",
- },
- ],
- // sideMenuDetection: "editor",
-} satisfies Partial<
- BlockNoteEditorOptions<
- typeof schema.blockSchema,
- typeof schema.inlineContentSchema,
- typeof schema.styleSchema
- >
->;
+ ],
+ });
-export default function App() {
- // Creates a new editor instance.
- const editor1 = useCreateBlockNote(options);
- const editor2 = useCreateBlockNote(options);
+ const slashMenuItems = useMemo(() => {
+ return combineByGroup(
+ getDefaultReactSlashMenuItems(editor),
+ getMultiColumnSlashMenuItems(editor)
+ );
+ }, [editor]);
// Renders the editor instance using a React component.
return (
-
-
- {/**/}
-
+
+ filterSuggestionItems(slashMenuItems, query)}
+ />
+
);
}
diff --git a/examples/01-basic/12-multi-editor/.bnexample.json b/examples/01-basic/12-multi-editor/.bnexample.json
new file mode 100644
index 000000000..54cfd2057
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "areknawo",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/12-multi-editor/App.tsx b/examples/01-basic/12-multi-editor/App.tsx
new file mode 100644
index 000000000..edc2a84c0
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/App.tsx
@@ -0,0 +1,55 @@
+import { PartialBlock } from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+// Component that creates & renders a BlockNote editor.
+function Editor(props: { initialContent?: PartialBlock[] }) {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ sideMenuDetection: "editor",
+ initialContent: props.initialContent,
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
+
+export default function App() {
+ // Creates & renders two editors side by side.
+ return (
+
+
+
+
+ );
+}
diff --git a/examples/01-basic/12-multi-editor/README.md b/examples/01-basic/12-multi-editor/README.md
new file mode 100644
index 000000000..3cee21f32
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/README.md
@@ -0,0 +1,7 @@
+# Multi-Editor Setup
+
+This example showcases use of multiple editors in a single page - you can even drag blocks between them.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/editor-basics/setup)
diff --git a/examples/01-basic/12-multi-editor/index.html b/examples/01-basic/12-multi-editor/index.html
new file mode 100644
index 000000000..f7f737083
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Multi-Editor Setup
+
+
+
+
+
+
diff --git a/examples/01-basic/12-multi-editor/main.tsx b/examples/01-basic/12-multi-editor/main.tsx
new file mode 100644
index 000000000..f88b490fb
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/12-multi-editor/package.json b/examples/01-basic/12-multi-editor/package.json
new file mode 100644
index 000000000..51045d2b1
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@blocknote/example-multi-editor",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --max-warnings 0"
+ },
+ "dependencies": {
+ "@blocknote/core": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/ariakit": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/shadcn": "latest",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
+ "@vitejs/plugin-react": "^4.3.1",
+ "eslint": "^8.10.0",
+ "vite": "^5.3.4"
+ },
+ "eslintConfig": {
+ "extends": [
+ "../../../.eslintrc.js"
+ ]
+ },
+ "eslintIgnore": [
+ "dist"
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/12-multi-editor/tsconfig.json b/examples/01-basic/12-multi-editor/tsconfig.json
new file mode 100644
index 000000000..1bd8ab3c5
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/12-multi-editor/vite.config.ts b/examples/01-basic/12-multi-editor/vite.config.ts
new file mode 100644
index 000000000..f62ab20bc
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts
index 0ed948595..2143d9f68 100644
--- a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts
+++ b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts
@@ -25,6 +25,10 @@ interface CodeBlockOptions {
supportedLanguages: SupportedLanguageConfig[];
}
+export const shikiParserSymbol = Symbol.for("blocknote.shikiParser");
+export const shikiHighlighterPromiseSymbol = Symbol.for(
+ "blocknote.shikiHighlighterPromise"
+);
export const defaultCodeBlockPropSchema = {
language: {
default: "javascript",
@@ -199,19 +203,30 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
};
},
addProseMirrorPlugins() {
+ const supportedLanguages = this.options
+ .supportedLanguages as SupportedLanguageConfig[];
+ const globalThisForShiki = globalThis as {
+ [shikiHighlighterPromiseSymbol]?: Promise;
+ [shikiParserSymbol]?: Parser;
+ };
+
let highlighter: Highlighter | undefined;
let parser: Parser | undefined;
- const supportedLanguages = this.options
- .supportedLanguages as SupportedLanguageConfig[];
const lazyParser: Parser = (options) => {
if (!highlighter) {
- return createHighlighter({
- themes: ["github-dark"],
- langs: [],
- }).then((createdHighlighter) => {
- highlighter = createdHighlighter;
- });
+ globalThisForShiki[shikiHighlighterPromiseSymbol] =
+ globalThisForShiki[shikiHighlighterPromiseSymbol] ||
+ createHighlighter({
+ themes: ["github-dark"],
+ langs: [],
+ });
+
+ return globalThisForShiki[shikiHighlighterPromiseSymbol].then(
+ (createdHighlighter) => {
+ highlighter = createdHighlighter;
+ }
+ );
}
const language = options.language;
@@ -227,7 +242,9 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
}
if (!parser) {
- parser = createParser(highlighter);
+ parser =
+ globalThisForShiki[shikiParserSymbol] || createParser(highlighter);
+ globalThisForShiki[shikiParserSymbol] = parser;
}
return parser(options);
diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx
index 03af43c99..35a8320c9 100644
--- a/playground/src/examples.gen.tsx
+++ b/playground/src/examples.gen.tsx
@@ -217,6 +217,24 @@
"slug": "basic"
}
},
+ {
+ "projectSlug": "multi-editor",
+ "fullSlug": "basic/multi-editor",
+ "pathFromRoot": "examples/01-basic/12-multi-editor",
+ "config": {
+ "playground": true,
+ "docs": true,
+ "author": "areknawo",
+ "tags": [
+ "Basic"
+ ]
+ },
+ "title": "Multi-Editor Setup",
+ "group": {
+ "pathFromRoot": "examples/01-basic",
+ "slug": "basic"
+ }
+ },
{
"projectSlug": "testing",
"fullSlug": "basic/testing",