Skip to content

Commit 454a91c

Browse files
authored
refactor: add renderEditor option to BlockNoteView (#1453)
* refactor: add renderEditor boolean to BlockNoteView * address feedback
1 parent 33a9d34 commit 454a91c

File tree

4 files changed

+157
-70
lines changed

4 files changed

+157
-70
lines changed

packages/core/src/util/browser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function formatKeyboardShortcut(shortcut: string, ctrlText = "Ctrl") {
1212
}
1313
}
1414

15-
export function mergeCSSClasses(...classes: string[]) {
15+
export function mergeCSSClasses(...classes: (string | undefined)[]) {
1616
return classes.filter((c) => c).join(" ");
1717
}
1818

packages/react/src/editor/BlockNoteContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from "@blocknote/core";
1111
import { createContext, useContext, useState } from "react";
1212

13-
type BlockNoteContextValue<
13+
export type BlockNoteContextValue<
1414
BSchema extends BlockSchema = DefaultBlockSchema,
1515
ISchema extends InlineContentSchema = DefaultInlineContentSchema,
1616
SSchema extends StyleSchema = DefaultStyleSchema

packages/react/src/editor/BlockNoteView.tsx

Lines changed: 136 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
StyleSchema,
66
mergeCSSClasses,
77
} from "@blocknote/core";
8-
98
import React, {
109
HTMLAttributes,
1110
ReactNode,
@@ -15,14 +14,23 @@ import React, {
1514
useMemo,
1615
useState,
1716
} from "react";
17+
import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor.js";
1818
import { useEditorChange } from "../hooks/useEditorChange.js";
1919
import { useEditorSelectionChange } from "../hooks/useEditorSelectionChange.js";
2020
import { usePrefersColorScheme } from "../hooks/usePrefersColorScheme.js";
21-
import { BlockNoteContext, useBlockNoteContext } from "./BlockNoteContext.js";
21+
import {
22+
BlockNoteContext,
23+
BlockNoteContextValue,
24+
useBlockNoteContext,
25+
} from "./BlockNoteContext.js";
2226
import {
2327
BlockNoteDefaultUI,
2428
BlockNoteDefaultUIProps,
2529
} from "./BlockNoteDefaultUI.js";
30+
import {
31+
BlockNoteViewContext,
32+
useBlockNoteViewContext,
33+
} from "./BlockNoteViewContext.js";
2634
import { Portals, getContentComponent } from "./EditorContent.js";
2735
import { ElementRenderer } from "./ElementRenderer.js";
2836
import "./styles.css";
@@ -40,8 +48,18 @@ export type BlockNoteViewProps<
4048

4149
theme?: "light" | "dark";
4250

51+
/**
52+
* Whether to render the editor element itself.
53+
* When `false`, you're responsible for rendering the editor yourself using the `BlockNoteViewEditor` component.
54+
*
55+
* @default true
56+
*/
57+
renderEditor?: boolean;
58+
4359
/**
4460
* Locks the editor from being editable by the user if set to `false`.
61+
*
62+
* @default true
4563
*/
4664
editable?: boolean;
4765
/**
@@ -86,6 +104,8 @@ function BlockNoteViewComponent<
86104
sideMenu,
87105
filePanel,
88106
tableHandles,
107+
autoFocus,
108+
renderEditor = !editor.headless,
89109
...rest
90110
} = props;
91111

@@ -109,83 +129,58 @@ function BlockNoteViewComponent<
109129
editor.isEditable = editable !== false;
110130
}, [editable, editor]);
111131

112-
const renderChildren = useMemo(() => {
113-
return (
114-
<>
115-
{children}
116-
<BlockNoteDefaultUI
117-
formattingToolbar={formattingToolbar}
118-
linkToolbar={linkToolbar}
119-
slashMenu={slashMenu}
120-
emojiPicker={emojiPicker}
121-
sideMenu={sideMenu}
122-
filePanel={filePanel}
123-
tableHandles={tableHandles}
124-
/>
125-
</>
126-
);
127-
}, [
128-
children,
129-
formattingToolbar,
130-
linkToolbar,
131-
slashMenu,
132-
emojiPicker,
133-
sideMenu,
134-
filePanel,
135-
tableHandles,
136-
]);
132+
const setElementRenderer = useCallback(
133+
(ref: (typeof editor)["elementRenderer"]) => {
134+
editor.elementRenderer = ref;
135+
},
136+
[editor]
137+
);
137138

138-
const context = useMemo(() => {
139+
// The BlockNoteContext makes sure the editor and some helper methods
140+
// are always available to nesteed compoenents
141+
const blockNoteContext: BlockNoteContextValue<any, any, any> = useMemo(() => {
139142
return {
140143
...existingContext,
141144
editor,
142145
setContentEditableProps,
143146
};
144147
}, [existingContext, editor]);
145148

146-
const setElementRenderer = useCallback(
147-
(ref: (typeof editor)["elementRenderer"]) => {
148-
editor.elementRenderer = ref;
149-
},
150-
[editor]
151-
);
152-
153-
const portalManager = useMemo(() => {
154-
return getContentComponent();
155-
}, []);
149+
// We set defaultUIProps and editorProps on a different context, the BlockNoteViewContext.
150+
// This BlockNoteViewContext is used to render the editor and the default UI.
151+
const defaultUIProps = {
152+
formattingToolbar,
153+
linkToolbar,
154+
slashMenu,
155+
emojiPicker,
156+
sideMenu,
157+
filePanel,
158+
tableHandles,
159+
};
156160

157-
const mount = useCallback(
158-
(element: HTMLElement | null) => {
159-
editor.mount(element, portalManager);
160-
},
161-
[editor, portalManager]
162-
);
161+
const editorProps = {
162+
autoFocus,
163+
className,
164+
editorColorScheme,
165+
contentEditableProps,
166+
ref,
167+
...rest,
168+
};
163169

164170
return (
165-
<BlockNoteContext.Provider value={context as any}>
166-
<ElementRenderer ref={setElementRenderer} />
167-
{!editor.headless && (
168-
<>
169-
<Portals contentComponent={portalManager} />
170-
<div
171-
className={mergeCSSClasses(
172-
"bn-container",
173-
editorColorScheme || "",
174-
className || ""
175-
)}
176-
data-color-scheme={editorColorScheme}
177-
{...rest}
178-
ref={ref}>
179-
<div
180-
aria-autocomplete="list"
181-
aria-haspopup="listbox"
182-
ref={mount}
183-
{...contentEditableProps}
184-
/>
185-
{renderChildren}
186-
</div>
187-
</>
188-
)}
171+
<BlockNoteContext.Provider value={blockNoteContext}>
172+
<BlockNoteViewContext.Provider
173+
value={{
174+
editorProps,
175+
defaultUIProps,
176+
}}>
177+
<ElementRenderer ref={setElementRenderer} />
178+
{renderEditor ? (
179+
<BlockNoteViewEditor>{children}</BlockNoteViewEditor>
180+
) : (
181+
children
182+
)}
183+
</BlockNoteViewContext.Provider>
189184
</BlockNoteContext.Provider>
190185
);
191186
}
@@ -200,3 +195,76 @@ export const BlockNoteViewRaw = React.forwardRef(BlockNoteViewComponent) as <
200195
ref?: React.ForwardedRef<HTMLDivElement>;
201196
}
202197
) => ReturnType<typeof BlockNoteViewComponent<BSchema, ISchema, SSchema>>;
198+
199+
/**
200+
* Renders the editor itself and the default UI elements
201+
*/
202+
export const BlockNoteViewEditor = (props: { children: ReactNode }) => {
203+
const ctx = useBlockNoteViewContext()!;
204+
const editor = useBlockNoteEditor();
205+
206+
const portalManager = useMemo(() => {
207+
return getContentComponent();
208+
}, []);
209+
210+
const mount = useCallback(
211+
(element: HTMLElement | null) => {
212+
editor.mount(element, portalManager);
213+
},
214+
[editor, portalManager]
215+
);
216+
217+
return (
218+
<>
219+
<Portals contentComponent={portalManager} />
220+
<EditorElement {...ctx.editorProps} {...props} mount={mount}>
221+
{/* Renders the UI elements such as formatting toolbar, etc, unless they have been explicitly disabled in defaultUIProps */}
222+
<BlockNoteDefaultUI {...ctx.defaultUIProps} />
223+
{/* Manually passed in children, such as customized UI elements / controllers */}
224+
{props.children}
225+
</EditorElement>
226+
</>
227+
);
228+
};
229+
230+
/**
231+
* Renders the container div + contentEditable div.
232+
*/
233+
const EditorElement = (
234+
props: {
235+
className?: string;
236+
editorColorScheme?: string;
237+
autoFocus?: boolean;
238+
mount: (element: HTMLElement | null) => void;
239+
contentEditableProps?: Record<string, any>;
240+
children: ReactNode;
241+
} & HTMLAttributes<HTMLDivElement>
242+
) => {
243+
const {
244+
className,
245+
editorColorScheme,
246+
autoFocus,
247+
mount,
248+
children,
249+
contentEditableProps,
250+
...rest
251+
} = props;
252+
return (
253+
// The container wraps the contentEditable div and UI Elements such as sidebar, formatting toolbar, etc.
254+
<div
255+
className={mergeCSSClasses("bn-container", editorColorScheme, className)}
256+
data-color-scheme={editorColorScheme}
257+
{...rest}>
258+
{/* The actual contentEditable that Prosemirror mounts to */}
259+
<div
260+
aria-autocomplete="list"
261+
aria-haspopup="listbox"
262+
data-bn-autofocus={autoFocus}
263+
ref={mount}
264+
{...contentEditableProps}
265+
/>
266+
{/* The UI elements such as sidebar, formatting toolbar, etc. */}
267+
{children}
268+
</div>
269+
);
270+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createContext, useContext } from "react";
2+
import { BlockNoteDefaultUIProps } from "./BlockNoteDefaultUI.js";
3+
4+
export type BlockNoteViewContextValue = {
5+
editorProps: any;
6+
defaultUIProps: BlockNoteDefaultUIProps;
7+
};
8+
9+
export const BlockNoteViewContext = createContext<
10+
BlockNoteViewContextValue | undefined
11+
>(undefined);
12+
13+
export function useBlockNoteViewContext():
14+
| BlockNoteViewContextValue
15+
| undefined {
16+
const context = useContext(BlockNoteViewContext) as any;
17+
18+
return context;
19+
}

0 commit comments

Comments
 (0)