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",