diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts
index e2ae4a7256..0178da4936 100644
--- a/packages/core/src/editor/BlockNoteEditor.ts
+++ b/packages/core/src/editor/BlockNoteEditor.ts
@@ -589,8 +589,11 @@ export class BlockNoteEditor<
*
* @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting
*/
- public mount = (parentElement?: HTMLElement | null) => {
- this._tiptapEditor.mount(parentElement);
+ public mount = (
+ parentElement?: HTMLElement | null,
+ contentComponent?: any
+ ) => {
+ this._tiptapEditor.mount(parentElement, contentComponent);
};
public get prosemirrorView() {
diff --git a/packages/core/src/editor/BlockNoteTipTapEditor.ts b/packages/core/src/editor/BlockNoteTipTapEditor.ts
index f4505d8153..de30b90047 100644
--- a/packages/core/src/editor/BlockNoteTipTapEditor.ts
+++ b/packages/core/src/editor/BlockNoteTipTapEditor.ts
@@ -21,10 +21,9 @@ export type BlockNoteTipTapEditorOptions = Partial<
* Custom Editor class that extends TiptapEditor and separates
* the creation of the view from the constructor.
*/
-// @ts-ignore
export class BlockNoteTipTapEditor extends TiptapEditor {
private _state: EditorState;
- private _creating = false;
+
public static create = (
options: BlockNoteTipTapEditorOptions,
styleSchema: StyleSchema
@@ -150,40 +149,34 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
/**
* Replace the default `createView` method with a custom one - which we call on mount
*/
- private createViewAlternative() {
- this._creating = true;
- // Without queueMicrotask, custom IC / styles will give a React FlushSync error
- queueMicrotask(() => {
- if (!this._creating) {
- return;
+ private createViewAlternative(contentComponent?: any) {
+ (this as any).contentComponent = contentComponent;
+
+ this.view = new EditorView(
+ { mount: this.options.element as any }, // use mount option so that we reuse the existing element instead of creating a new one
+ {
+ ...this.options.editorProps,
+ // @ts-ignore
+ dispatchTransaction: this.dispatchTransaction.bind(this),
+ state: this.state,
}
- this.view = new EditorView(
- { mount: this.options.element as any }, // use mount option so that we reuse the existing element instead of creating a new one
- {
- ...this.options.editorProps,
- // @ts-ignore
- dispatchTransaction: this.dispatchTransaction.bind(this),
- state: this.state,
- }
- );
+ );
- // `editor.view` is not yet available at this time.
- // Therefore we will add all plugins and node views directly afterwards.
- const newState = this.state.reconfigure({
- plugins: this.extensionManager.plugins,
- });
+ // `editor.view` is not yet available at this time.
+ // Therefore we will add all plugins and node views directly afterwards.
+ const newState = this.state.reconfigure({
+ plugins: this.extensionManager.plugins,
+ });
- this.view.updateState(newState);
+ this.view.updateState(newState);
- this.createNodeViews();
+ this.createNodeViews();
- // emit the created event, call here manually because we blocked the default call in the constructor
- // (https://github.com/ueberdosis/tiptap/blob/45bac803283446795ad1b03f43d3746fa54a68ff/packages/core/src/Editor.ts#L117)
- this.commands.focus(this.options.autofocus);
- this.emit("create", { editor: this });
- this.isInitialized = true;
- this._creating = false;
- });
+ // emit the created event, call here manually because we blocked the default call in the constructor
+ // (https://github.com/ueberdosis/tiptap/blob/45bac803283446795ad1b03f43d3746fa54a68ff/packages/core/src/Editor.ts#L117)
+ this.commands.focus(this.options.autofocus);
+ this.emit("create", { editor: this });
+ this.isInitialized = true;
}
/**
@@ -191,15 +184,12 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
*
* @param element DOM element to mount to, ur null / undefined to destroy
*/
- public mount = (element?: HTMLElement | null) => {
+ public mount = (element?: HTMLElement | null, contentComponent?: any) => {
if (!element) {
this.destroy();
- // cancel pending microtask
- this._creating = false;
} else {
this.options.element = element;
- // @ts-ignore
- this.createViewAlternative();
+ this.createViewAlternative(contentComponent);
}
};
}
@@ -210,5 +200,6 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
// We should call `createView` manually only when a DOM element is available
// additional fix because onPaste and onDrop depend on installing plugins in constructor which we don't support
+ // (note: can probably be removed after tiptap upgrade fixed in 2.8.0)
this.options.onPaste = this.options.onDrop = undefined;
};
diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx
index 7423810eb9..a18b10e49d 100644
--- a/packages/react/src/editor/BlockNoteView.tsx
+++ b/packages/react/src/editor/BlockNoteView.tsx
@@ -23,7 +23,7 @@ import {
BlockNoteDefaultUI,
BlockNoteDefaultUIProps,
} from "./BlockNoteDefaultUI.js";
-import { EditorContent } from "./EditorContent.js";
+import { Portals, getContentComponent } from "./EditorContent.js";
import { ElementRenderer } from "./ElementRenderer.js";
import "./styles.css";
@@ -150,11 +150,23 @@ function BlockNoteViewComponent<
[editor]
);
+ const portalManager = useMemo(() => {
+ return getContentComponent();
+ }, []);
+
+ const mount = useCallback(
+ (element: HTMLElement | null) => {
+ editor.mount(element, portalManager);
+ },
+ [editor, portalManager]
+ );
+
return (
{!editor.headless && (
-
+ <>
+
{renderChildren}
-
+ >
)}
);
diff --git a/packages/react/src/editor/EditorContent.tsx b/packages/react/src/editor/EditorContent.tsx
index 1bd6c9accb..10727789b4 100644
--- a/packages/react/src/editor/EditorContent.tsx
+++ b/packages/react/src/editor/EditorContent.tsx
@@ -1,67 +1,69 @@
-import { BlockNoteEditor } from "@blocknote/core";
import { ReactRenderer } from "@tiptap/react";
-import { useEffect, useState } from "react";
+import { useSyncExternalStore } from "react";
import { createPortal } from "react-dom";
-const Portals: React.FC<{ renderers: Record }> = ({
- renderers,
-}) => {
- return (
- <>
- {Object.entries(renderers).map(([key, renderer]) => {
- return createPortal(renderer.reactElement, renderer.element, key);
- })}
- >
- );
-};
+// this file takes the methods we need from
+// https://github.com/ueberdosis/tiptap/blob/develop/packages/react/src/EditorContent.tsx
-/**
- * Replacement of https://github.com/ueberdosis/tiptap/blob/6676c7e034a46117afdde560a1b25fe75411a21d/packages/react/src/EditorContent.tsx
- * that only takes care of the Portals.
- *
- * Original implementation is messy, and we use a "mount" system in BlockNoteTiptapEditor.tsx that makes this cleaner
- */
-export function EditorContent(props: {
- editor: BlockNoteEditor;
- children: any;
-}) {
- const [renderers, setRenderers] = useState>({});
+export function getContentComponent() {
+ const subscribers = new Set<() => void>();
+ let renderers: Record = {};
- useEffect(() => {
- props.editor._tiptapEditor.contentComponent = {
- /**
- * Used by TipTap
- */
- setRenderer(id: string, renderer: ReactRenderer) {
- setRenderers((renderers) => ({ ...renderers, [id]: renderer }));
- },
+ return {
+ /**
+ * Subscribe to the editor instance's changes.
+ */
+ subscribe(callback: () => void) {
+ subscribers.add(callback);
+ return () => {
+ subscribers.delete(callback);
+ };
+ },
+ getSnapshot() {
+ return renderers;
+ },
+ getServerSnapshot() {
+ return renderers;
+ },
+ /**
+ * Adds a new NodeView Renderer to the editor.
+ */
+ setRenderer(id: string, renderer: ReactRenderer) {
+ renderers = {
+ ...renderers,
+ [id]: createPortal(renderer.reactElement, renderer.element, id),
+ };
- /**
- * Used by TipTap
- */
- removeRenderer(id: string) {
- setRenderers((renderers) => {
- const nextRenderers = { ...renderers };
+ subscribers.forEach((subscriber) => subscriber());
+ },
+ /**
+ * Removes a NodeView Renderer from the editor.
+ */
+ removeRenderer(id: string) {
+ const nextRenderers = { ...renderers };
- delete nextRenderers[id];
+ delete nextRenderers[id];
+ renderers = nextRenderers;
+ subscribers.forEach((subscriber) => subscriber());
+ },
+ };
+}
- return nextRenderers;
- });
- },
- };
- // Without queueMicrotask, custom IC / styles will give a React FlushSync error
- queueMicrotask(() => {
- props.editor._tiptapEditor.createNodeViews();
- });
- return () => {
- props.editor._tiptapEditor.contentComponent = null;
- };
- }, [props.editor._tiptapEditor]);
+type ContentComponent = ReturnType;
- return (
- <>
-
- {props.children}
- >
+/**
+ * This component renders all of the editor's node views.
+ */
+export const Portals: React.FC<{ contentComponent: ContentComponent }> = ({
+ contentComponent,
+}) => {
+ // For performance reasons, we render the node view portals on state changes only
+ const renderers = useSyncExternalStore(
+ contentComponent.subscribe,
+ contentComponent.getSnapshot,
+ contentComponent.getServerSnapshot
);
-}
+
+ // This allows us to directly render the portals without any additional wrapper
+ return <>{Object.values(renderers)}>;
+};