- {/*Input for link text*/}
- setText(event.target.value)}
- />
- {/*Input for link URL*/}
- setUrl(event.target.value)}
- />
- {/*Buttons to create and clear the inputs*/}
-
-
-
-
-
- );
-};
diff --git a/examples/03-ui-components/12-custom-ui/README.md b/examples/03-ui-components/12-custom-ui/README.md
deleted file mode 100644
index 3dc19a4d2d..0000000000
--- a/examples/03-ui-components/12-custom-ui/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Custom UI
-
-In this example, we replace the default Formatting Toolbar using a custom React component, as well as the default Slash Menu and Side Menu. The Formatting Toolbar is also made static and always visible above the editor.
-
-**Relevant Docs:**
-
-- [Formatting Toolbar](/docs/ui-components/formatting-toolbar)
-- [Manipulating Inline Content](/docs/editor-api/manipulating-inline-content)
-- [Hooks](TODO)
-- [Slash Menu](/docs/ui-components/suggestion-menus#slash-menu)
-- [Side Menu](/docs/ui-components/side-menu)
-- [Editor Setup](/docs/editor-basics/setup)
\ No newline at end of file
diff --git a/examples/03-ui-components/12-custom-ui/styles.css b/examples/03-ui-components/12-custom-ui/styles.css
deleted file mode 100644
index 294d9864e0..0000000000
--- a/examples/03-ui-components/12-custom-ui/styles.css
+++ /dev/null
@@ -1,277 +0,0 @@
-.bn-container * {
- font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont,
- "Open Sans", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
- "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-}
-
-.color-menu {
- position: absolute;
- z-index: 9999;
-
- background-color: white;
- border: 1px solid lightgray;
- border-radius: 2px;
-
- display: flex;
- flex-direction: column;
- gap: 4px;
-
- margin-top: 8px;
- padding: 4px;
-}
-
-.color-menu-group {
- display: flex;
- flex-direction: row;
- gap: 4px;
-}
-
-.color-menu-item {
- position: relative;
-
- background-color: white;
- border: 1px solid lightgray;
- border-radius: 2px;
- box-shadow: 0 0 4px #dddddd;
-
- align-items: center;
- display: flex;
- flex-direction: row;
- gap: 4px;
- justify-content: center;
-
- padding: 0;
-
- height: 24px;
- width: 24px;
-}
-
-.text.red {
- color: #e03e3e;
-}
-
-.text.orange {
- color: #d9730d;
-}
-
-.text.yellow {
- color: #dfab01;
-}
-
-.text.green {
- color: #4d6461;
-}
-
-.text.blue {
- color: #0b6e99;
-}
-
-.text.purple {
- color: #6940a5;
-}
-
-.background.red {
- background-color: #fbe4e4;
-}
-
-.background.orange {
- background-color: #f6e9d9;
-}
-
-.background.yellow {
- background-color: #fbf3db;
-}
-
-.background.green {
- background-color: #ddedea;
-}
-
-.background.blue {
- background-color: #ddebf1;
-}
-
-.background.purple {
- background-color: #eae4f2;
-}
-
-.color-menu-item.text:hover {
- background-color: lightgray;
-}
-
-.color-menu-item.background:hover {
- color: gray;
-}
-
-.link-menu {
- position: absolute;
- z-index: 9999;
-
- background-color: white;
- border: 1px solid lightgray;
- border-radius: 2px;
-
- display: flex;
- flex-direction: column;
- gap: 4px;
-
- margin-top: 8px;
- padding: 4px;
-}
-
-.link-input {
- border: 1px solid lightgray;
- border-radius: 2px;
- box-shadow: 0 0 4px #dddddd;
-
- padding: 4px;
-}
-
-.link-buttons {
- display: flex;
- flex-direction: row;
- gap: 4px;
-}
-
-.link-button {
- background-color: white;
- border: 1px solid lightgray;
- border-radius: 2px;
- box-shadow: 0 0 4px #dddddd;
-
- cursor: pointer;
-
- font-size: 14px;
-
- flex-grow: 1;
-}
-
-.link-button:hover {
- background-color: lightgray;
-}
-
-.formatting-toolbar {
- position: sticky;
- z-index: 9999;
-
- background-color: white;
- border: 1px solid lightgray;
- border-radius: 2px;
- box-shadow: 0 0 8px #dddddd;
-
- display: flex;
- flex-direction: row;
- gap: 16px;
-
- margin-inline: 54px;
- margin-bottom: 8px;
- padding: 4px;
-
- top: 8px;
-}
-
-.formatting-toolbar-group {
- display: flex;
- flex-direction: row;
- gap: 4px;
-}
-
-.formatting-toolbar-button {
- background-color: white;
- border: 1px solid lightgray;
- border-radius: 2px;
- box-shadow: 0 0 4px #dddddd;
-
- cursor: pointer;
-
- font-size: 16px;
-
- align-items: center;
- display: flex;
- justify-content: center;
-
- height: 32px;
- width: 32px;
-}
-
-.formatting-toolbar-button:hover {
- background-color: lightgray;
-}
-
-.slash-menu {
- z-index: 9999;
-
- background-color: white;
- border: 1px solid lightgray;
- border-radius: 2px;
- box-shadow: 0 0 8px #dddddd;
-
- display: flex;
- flex-direction: column;
- gap: 8px;
-
- padding: 8px;
-
- top: 8px;
-}
-
-.slash-menu-group {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.slash-menu-label {
- color: gray;
- font-size: 12px;
-}
-
-.slash-menu-item-group {
- display: flex;
- flex-direction: row;
- gap: 4px;
-}
-
-.slash-menu-item {
- background-color: white;
- border: 1px solid lightgray;
- border-radius: 2px;
- box-shadow: 0 0 4px #dddddd;
-
- cursor: pointer;
-
- font-size: 16px;
-
- align-items: center;
- display: flex;
- flex-direction: row;
-
- padding: 8px;
-}
-
-.slash-menu-item:hover {
- background-color: lightgray;
-}
-
-.side-menu {
- background-color: white;
- border: 1px solid lightgray;
- border-radius: 2px;
- box-shadow: 0 0 4px #dddddd;
-
- cursor: pointer;
-
- align-items: center;
- display: flex;
- justify-content: center;
-
- margin-right: 4px;
- padding: 8px;
-}
-
-.active {
- box-shadow: inset 0 0 6px #cccccc;
-}
-
-.hidden {
- display: none;
-}
\ No newline at end of file
diff --git a/examples/03-ui-components/12-static-formatting-toolbar/.bnexample.json b/examples/03-ui-components/12-static-formatting-toolbar/.bnexample.json
new file mode 100644
index 0000000000..0c02809402
--- /dev/null
+++ b/examples/03-ui-components/12-static-formatting-toolbar/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Basic", "UI Components", "Formatting Toolbar", "Appearance & Styling"]
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/12-static-formatting-toolbar/App.tsx b/examples/03-ui-components/12-static-formatting-toolbar/App.tsx
new file mode 100644
index 0000000000..fa61f64dc6
--- /dev/null
+++ b/examples/03-ui-components/12-static-formatting-toolbar/App.tsx
@@ -0,0 +1,38 @@
+import "@blocknote/core/fonts/inter.css";
+import { FormattingToolbar, useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+
+import "./style.css";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content: "Check out the static formatting toolbar above!",
+ },
+ {
+ type: "paragraph",
+ },
+ ],
+ });
+
+ // Renders the editor instance using a React component.
+ return (
+ // Disables the default formatting toolbar and re-adds it without the
+ // `FormattingToolbarController` component. You may have seen
+ // `FormattingToolbarController` used in other examples, but we omit it here
+ // as we want to control the position and visibility ourselves. BlockNote
+ // also uses the `FormattingToolbarController` when displaying the
+ // Formatting Toolbar by default.
+
+
+
+ );
+}
diff --git a/examples/03-ui-components/12-static-formatting-toolbar/README.md b/examples/03-ui-components/12-static-formatting-toolbar/README.md
new file mode 100644
index 0000000000..5c778dc3bc
--- /dev/null
+++ b/examples/03-ui-components/12-static-formatting-toolbar/README.md
@@ -0,0 +1,9 @@
+# Static Formatting Toolbar
+
+This example shows how to make the formatting toolbar always visible and static
+above the editor.
+
+**Relevant Docs:**
+
+- [Changing the Formatting Toolbar](/docs/ui-components/formatting-toolbar#changing-the-formatting-toolbar)
+- [Editor Setup](/docs/editor-basics/setup)
diff --git a/examples/03-ui-components/12-custom-ui/index.html b/examples/03-ui-components/12-static-formatting-toolbar/index.html
similarity index 88%
rename from examples/03-ui-components/12-custom-ui/index.html
rename to examples/03-ui-components/12-static-formatting-toolbar/index.html
index e306e7cb45..3fc4daa8f4 100644
--- a/examples/03-ui-components/12-custom-ui/index.html
+++ b/examples/03-ui-components/12-static-formatting-toolbar/index.html
@@ -5,7 +5,7 @@
- Custom UI
+ Static Formatting Toolbar
diff --git a/examples/03-ui-components/12-custom-ui/main.tsx b/examples/03-ui-components/12-static-formatting-toolbar/main.tsx
similarity index 100%
rename from examples/03-ui-components/12-custom-ui/main.tsx
rename to examples/03-ui-components/12-static-formatting-toolbar/main.tsx
diff --git a/examples/03-ui-components/12-custom-ui/package.json b/examples/03-ui-components/12-static-formatting-toolbar/package.json
similarity index 88%
rename from examples/03-ui-components/12-custom-ui/package.json
rename to examples/03-ui-components/12-static-formatting-toolbar/package.json
index 07a11b988e..368e763cbd 100644
--- a/examples/03-ui-components/12-custom-ui/package.json
+++ b/examples/03-ui-components/12-static-formatting-toolbar/package.json
@@ -1,5 +1,5 @@
{
- "name": "@blocknote/example-custom-ui",
+ "name": "@blocknote/example-static-formatting-toolbar",
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
"private": true,
"version": "0.12.4",
@@ -17,8 +17,7 @@
"@blocknote/mantine": "latest",
"@blocknote/shadcn": "latest",
"react": "^18.3.1",
- "react-dom": "^18.3.1",
- "react-icons": "^5.2.1"
+ "react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.0.25",
diff --git a/examples/03-ui-components/12-static-formatting-toolbar/style.css b/examples/03-ui-components/12-static-formatting-toolbar/style.css
new file mode 100644
index 0000000000..839dd7baa0
--- /dev/null
+++ b/examples/03-ui-components/12-static-formatting-toolbar/style.css
@@ -0,0 +1,9 @@
+.bn-container {
+ display: flex;
+ flex-direction: column-reverse;
+ gap: 8px;
+}
+
+.bn-formatting-toolbar {
+ margin-inline: auto;
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/12-custom-ui/tsconfig.json b/examples/03-ui-components/12-static-formatting-toolbar/tsconfig.json
similarity index 100%
rename from examples/03-ui-components/12-custom-ui/tsconfig.json
rename to examples/03-ui-components/12-static-formatting-toolbar/tsconfig.json
diff --git a/examples/03-ui-components/12-custom-ui/vite.config.ts b/examples/03-ui-components/12-static-formatting-toolbar/vite.config.ts
similarity index 100%
rename from examples/03-ui-components/12-custom-ui/vite.config.ts
rename to examples/03-ui-components/12-static-formatting-toolbar/vite.config.ts
diff --git a/examples/03-ui-components/12-custom-ui/.bnexample.json b/examples/03-ui-components/13-custom-ui/.bnexample.json
similarity index 65%
rename from examples/03-ui-components/12-custom-ui/.bnexample.json
rename to examples/03-ui-components/13-custom-ui/.bnexample.json
index 6c50d66ec7..6969cbc23a 100644
--- a/examples/03-ui-components/12-custom-ui/.bnexample.json
+++ b/examples/03-ui-components/13-custom-ui/.bnexample.json
@@ -4,7 +4,10 @@
"author": "matthewlipski",
"tags": ["Advanced", "Inline Content", "UI Components", "Block Side Menu", "Formatting Toolbar", "Suggestion Menus", "Slash Menu", "Appearance & Styling"],
"dependencies": {
- "react-icons": "^5.2.1"
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "@mui/icons-material": "^5.16.1",
+ "@mui/material": "^5.16.1"
},
"pro": true
}
\ No newline at end of file
diff --git a/examples/03-ui-components/13-custom-ui/App.tsx b/examples/03-ui-components/13-custom-ui/App.tsx
new file mode 100644
index 0000000000..f2892ae0d4
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/App.tsx
@@ -0,0 +1,90 @@
+import { filterSuggestionItems } from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import {
+ BlockNoteViewRaw,
+ getDefaultReactSlashMenuItems,
+ SideMenuController,
+ SuggestionMenuController,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import "@blocknote/react/style.css";
+import { createTheme, ThemeProvider, useMediaQuery } from "@mui/material";
+import { useMemo } from "react";
+
+import { schema } from "./schema";
+import { CustomMUIFormattingToolbar } from "./MUIFormattingToolbar";
+import { CustomMUISideMenu } from "./MUISideMenu";
+import { MUISuggestionMenu } from "./MUISuggestionMenu";
+
+import "./style.css";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ schema,
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ },
+ ],
+ });
+
+ // Automatically sets light/dark mode based on the user's system preferences.
+ const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
+ const theme = useMemo(
+ () =>
+ createTheme({
+ palette: {
+ mode: prefersDarkMode ? "dark" : "light",
+ },
+ }),
+ [prefersDarkMode]
+ );
+
+ // Renders the editor instance.
+ return (
+ // Provides theming for Material UI.
+
+
+ {/* Adds the custom Formatting Toolbar. */}
+ {/* `FormattingToolbarController isn't used since we make the custom */}
+ {/* toolbar static and always visible above the editor for this */}
+ {/* example. */}
+
+ {/* Adds the custom Side Menu and Slash Menu. */}
+ {/* These use controllers since we want them to be positioned and */}
+ {/* show/hide the same as the default ones. */}
+
+
+ filterSuggestionItems(
+ getDefaultReactSlashMenuItems(editor).filter(
+ (item) => item.title !== "Emoji"
+ ),
+ query
+ )
+ }
+ suggestionMenuComponent={MUISuggestionMenu}
+ onItemClick={(i) => i.onItemClick()}
+ />
+
+
+ );
+}
diff --git a/examples/03-ui-components/13-custom-ui/MUIFormattingToolbar.tsx b/examples/03-ui-components/13-custom-ui/MUIFormattingToolbar.tsx
new file mode 100644
index 0000000000..4c464ebd56
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/MUIFormattingToolbar.tsx
@@ -0,0 +1,490 @@
+import { Block } from "@blocknote/core";
+import {
+ blockTypeSelectItems,
+ useBlockNoteEditor,
+ useEditorContentOrSelectionChange,
+} from "@blocknote/react";
+import {
+ Done,
+ FormatAlignCenter,
+ FormatAlignLeft,
+ FormatAlignRight,
+ FormatBold,
+ FormatColorText,
+ FormatItalic,
+ FormatStrikethrough,
+ FormatUnderlined,
+} from "@mui/icons-material";
+import {
+ AppBar,
+ Box,
+ Button,
+ ButtonGroup,
+ Container,
+ Divider,
+ FormControl,
+ ListItemIcon,
+ ListItemText,
+ Menu,
+ MenuItem,
+ Select,
+ SelectChangeEvent,
+ Toolbar,
+ Tooltip,
+ Typography,
+} from "@mui/material";
+import {
+ MouseEvent,
+ useCallback,
+ useState,
+ useMemo,
+ FC,
+ ReactNode,
+} from "react";
+
+import { TextBlockSchema } from "./schema";
+
+// This replaces the generic Mantine `ToolbarSelect` component with a simplified
+// MUI version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/mantine/src/toolbar/ToolbarSelect.tsx
+// In this example, we use it to create a replacement for the default Formatting
+// Toolbar select element (i.e. the Block Type Select) using MUI, but you can
+// also use it to add custom select elements.
+function MUIToolbarSelect(props: {
+ items: Item[];
+ selectedItem: Item;
+ onChange: (event: SelectChangeEvent) => void;
+}) {
+ return (
+
+ `${
+ theme.palette.mode === "dark"
+ ? theme.palette.primary.main
+ : theme.palette.background.default
+ } !important`,
+ borderColor: (theme) =>
+ `${
+ theme.palette.mode === "dark"
+ ? theme.palette.primary.main
+ : theme.palette.background.default
+ } !important`,
+ },
+ }}>
+
+
+ );
+}
+
+// This replaces the default `BlockTypeSelect` component with a simplified MUI
+// version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx
+function MUIBlockTypeSelect() {
+ const editor = useBlockNoteEditor();
+
+ // The block currently containing the text cursor.
+ const [block, setBlock] = useState(
+ editor.getTextCursorPosition().block
+ );
+
+ // Updates the block currently containing the text cursor whenever the editor
+ // content or selection changes.
+ useEditorContentOrSelectionChange(
+ () => setBlock(editor.getTextCursorPosition().block),
+ editor
+ );
+
+ // Gets the default items for the select.
+ const defaultBlockTypeSelectItems = useMemo(
+ () => blockTypeSelectItems(editor.dictionary),
+ [editor.dictionary]
+ );
+
+ // Gets the selected item.
+ const selectedItem = useMemo(
+ () =>
+ defaultBlockTypeSelectItems.find((item) =>
+ item.isSelected(block as any)
+ )!,
+ [defaultBlockTypeSelectItems, block]
+ );
+
+ // Updates the state when the user chooses an item.
+ const onChange = useCallback(
+ (event: SelectChangeEvent) => {
+ const newSelectedItem = defaultBlockTypeSelectItems.find(
+ (item) => item.name === event.target.value
+ )!;
+
+ editor.updateBlock(block, {
+ type: newSelectedItem.type as keyof TextBlockSchema,
+ props: newSelectedItem.props,
+ });
+ editor.focus();
+
+ setBlock(editor.getTextCursorPosition().block);
+ },
+ [block, defaultBlockTypeSelectItems, editor]
+ );
+
+ return (
+
+ );
+}
+
+// This replaces the generic Mantine `ToolbarButton` component with a simplified
+// MUI version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/mantine/src/toolbar/ToolbarButton.tsx
+// In this example, we use it to create replacements for the default Formatting
+// Toolbar buttons using MUI, but you can also use it to add custom buttons.
+function MUIToolbarButton(props: {
+ tooltip: string;
+ selected?: boolean;
+ onClick: (event: MouseEvent) => void;
+ children: ReactNode;
+}) {
+ return (
+
+
+
+ );
+}
+
+const basicTextStyleIcons = {
+ bold: FormatBold,
+ italic: FormatItalic,
+ underline: FormatUnderlined,
+ strike: FormatStrikethrough,
+};
+
+// This replaces the default `BasicTextStyleButton` component with a simplified
+// MUI version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/FormattingToolbar/DefaultButtons/BasicTextStyleButton.tsx
+function MUIBasicTextStyleButton(props: {
+ textStyle: "bold" | "italic" | "underline" | "strike";
+}) {
+ const Icon = basicTextStyleIcons[props.textStyle];
+ const editor = useBlockNoteEditor();
+
+ // Whether the text style is currently active.
+ const [textStyleActive, setTextStyleActive] = useState(
+ !!editor.getActiveStyles()[props.textStyle]
+ );
+
+ // Updates whether the text style is active when the editor content or
+ // selection changes.
+ useEditorContentOrSelectionChange(
+ () => setTextStyleActive(props.textStyle in editor.getActiveStyles()),
+ editor
+ );
+
+ // Tooltip for the button.
+ const tooltip = useMemo(
+ () =>
+ `Toggle ${props.textStyle
+ .slice(0, 1)
+ .toUpperCase()}${props.textStyle.slice(1)}`,
+ [props.textStyle]
+ );
+
+ // Toggles the text style when the button is clicked.
+ const onClick = useCallback(() => {
+ editor.toggleStyles({ [props.textStyle]: true });
+ editor.focus();
+ }, [editor, props.textStyle]);
+
+ return (
+
+
+
+ );
+}
+
+const textAlignIcons = {
+ left: FormatAlignLeft,
+ center: FormatAlignCenter,
+ right: FormatAlignRight,
+};
+
+// This replaces the default `TextAlignButton` component with a simplified MUI
+// version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx
+function MUITextAlignButton(props: {
+ textAlignment: "left" | "center" | "right";
+}) {
+ const Icon = textAlignIcons[props.textAlignment];
+ const editor = useBlockNoteEditor();
+
+ // The text alignment of the block currently containing the text cursor.
+ const [activeTextAlignment, setActiveTextAlignment] = useState(
+ () => editor.getTextCursorPosition().block.props.textAlignment
+ );
+
+ // Updates the text alignment when the editor content or selection changes.
+ useEditorContentOrSelectionChange(
+ () =>
+ setActiveTextAlignment(
+ editor.getTextCursorPosition().block.props.textAlignment
+ ),
+ editor
+ );
+
+ // Tooltip for the button.
+ const tooltip = useMemo(
+ () =>
+ `Align ${props.textAlignment
+ .slice(0, 1)
+ .toUpperCase()}${props.textAlignment.slice(1)}`,
+ [props.textAlignment]
+ );
+
+ // Sets the text alignment of the block currently containing the text cursor
+ // when the button is clicked.
+ const onClick = useCallback(() => {
+ editor.updateBlock(editor.getTextCursorPosition().block, {
+ props: { textAlignment: props.textAlignment },
+ });
+ editor.focus();
+ }, [editor, props.textAlignment]);
+
+ return (
+
+
+
+ );
+}
+
+// The highlight colors used by BlockNote.
+const colors = [
+ "default",
+ "red",
+ "orange",
+ "yellow",
+ "green",
+ "blue",
+ "purple",
+] as const;
+
+// This replaces the default `ColorStyleButton` component with a simplified MUI
+// version. The original component can be found here:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/FormattingToolbar/DefaultButtons/ColorStyleButton.tsx
+function MUIColorStyleButton() {
+ const editor = useBlockNoteEditor();
+
+ // Anchor/trigger element for the color menu.
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ // The active text and background colors.
+ const [activeTextColor, setActiveTextColor] = useState(
+ () => editor.getActiveStyles().textColor || "default"
+ );
+ const [activeBackgroundColor, setActiveBackgroundColor] = useState(
+ () => editor.getActiveStyles().backgroundColor || "default"
+ );
+
+ // Updates the active text and background colors when the editor content or
+ // selection changes.
+ useEditorContentOrSelectionChange(() => {
+ const activeStyles = editor.getActiveStyles();
+
+ setActiveTextColor(activeStyles.textColor || "default");
+ setActiveBackgroundColor(activeStyles.backgroundColor || "default");
+ }, editor);
+
+ // Handles opening and closing the color menu.
+ const onClick = useCallback(
+ (event: MouseEvent) => setAnchorEl(event.currentTarget),
+ []
+ );
+ const onClose = useCallback(() => setAnchorEl(null), []);
+
+ // Set the text or background color and close the color menu when a color is
+ // clicked.
+ const textColorOnClick = useCallback(
+ (textColor: string) => {
+ setAnchorEl(null);
+ textColor === "default"
+ ? editor.removeStyles({ textColor })
+ : editor.addStyles({ textColor });
+ setTimeout(() => editor.focus());
+ },
+ [editor]
+ );
+ const backgroundColorOnClick = useCallback(
+ (backgroundColor: string) => {
+ setAnchorEl(null);
+ backgroundColor === "default"
+ ? editor.removeStyles({ backgroundColor })
+ : editor.addStyles({ backgroundColor });
+ setTimeout(() => editor.focus());
+ },
+ [editor]
+ );
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+// This replaces the generic Mantine `Toolbar` component:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/mantine/src/toolbar/ToolbarSelect.tsx
+// In this example, we use it to create a replacement for the default Formatting
+// Toolbar using MUI, but you can also use it to replace the default Link
+// Toolbar.
+function MUIToolbar(props: { children?: ReactNode }) {
+ return (
+
+
+
+ {props.children}
+
+
+
+ );
+}
+
+// This replaces the default `FormattingToolbar` component with a simplified MUI
+// version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx
+// You can remove any of the default selects/buttons, or add custom
+// ones as children of the `MUIToolbar` component here.
+export function CustomMUIFormattingToolbar() {
+ return (
+
+
+
+ {/* Replaces the `BasicTextStyleButton` component: */}
+
+
+
+
+
+
+ {/* Replaces the `TextAlignButton` component: */}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/03-ui-components/13-custom-ui/MUISideMenu.tsx b/examples/03-ui-components/13-custom-ui/MUISideMenu.tsx
new file mode 100644
index 0000000000..6069094c4b
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/MUISideMenu.tsx
@@ -0,0 +1,165 @@
+import { SideMenuProps } from "@blocknote/react";
+import { Delete, DragIndicator } from "@mui/icons-material";
+import {
+ Box,
+ IconButton,
+ ListItemIcon,
+ ListItemText,
+ Menu,
+ MenuItem,
+ Typography,
+} from "@mui/material";
+import { MouseEvent, ReactNode, useCallback, useMemo, useState } from "react";
+
+import { TextBlockSchema } from "./schema";
+
+// This replaces the default `RemoveBlockItem` component with a simplified
+// MUI version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/RemoveBlockItem.tsx
+function MUIRemoveBlockItem(
+ props: SideMenuProps & { closeDragHandleMenu: () => void }
+) {
+ // Deletes the block next to the side menu.
+ const onClick = useCallback(() => {
+ props.unfreezeMenu();
+ props.closeDragHandleMenu();
+ props.editor.removeBlocks([props.editor.getTextCursorPosition().block]);
+ props.editor.focus();
+ }, [props]);
+
+ return (
+
+ );
+}
+
+// This replaces the default `DragHandleMenu` component with a simplified MUI
+// version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/DragHandleMenu/DragHandleMenu.tsx
+function MUIDragHandleMenu(props: {
+ anchorEl: HTMLElement | null;
+ container: Element;
+ onClose: () => void;
+ children: ReactNode;
+}) {
+ return (
+
+ );
+}
+
+// This replaces the default `DragHandleButton` component with a simplified MUI
+// version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/DefaultButtons/DragHandleButton.tsx
+function MUIDragHandleButton(props: SideMenuProps) {
+ // Anchor/trigger element for the color menu.
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ // Handles opening and closing the drag handle menu.
+ const onClick = useCallback(
+ (event: MouseEvent) => {
+ props.freezeMenu();
+ setAnchorEl(event.currentTarget);
+ },
+ [props]
+ );
+ const onClose = useCallback(() => {
+ setAnchorEl(null);
+ }, []);
+
+ return (
+ <>
+
+ theme.palette.text.primary,
+ }}
+ />
+
+
+
+
+ >
+ );
+}
+
+// This replaces the generic Mantine `SideMenu` component:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/mantine/src/sideMenu/SideMenu.tsx
+function MUISideMenu(
+ props: SideMenuProps & { children: ReactNode }
+) {
+ // Since the side menu is positioned by the top-left corner of a block, we
+ // manually set its height based on the hovered block so that it's vertically
+ // centered.
+ const sideMenuHeight = useMemo(() => {
+ if (props.block.type === "heading") {
+ if (props.block.props.level === 1) {
+ return 78;
+ }
+
+ if (props.block.props.level === 2) {
+ return 54;
+ }
+
+ if (props.block.props.level === 3) {
+ return 37;
+ }
+ }
+
+ return 30;
+ }, [props.block]);
+
+ return (
+
+ {props.children}
+
+ );
+}
+
+// This replaces the default `SideMenu` component with a simplified MUI version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/SideMenu.tsx
+// You can add to or replace the `MUIDragHandleButton` component using the MUI
+// `Button` components. Unlike the Formatting Toolbar, we don't use button
+// components specific to the Side Menu since there is really nothing more to
+// them than just an MUI `IconButton` and some styles passed via the `sx` prop,
+// as you can see in `MUIDragHandleButton`.
+export function CustomMUISideMenu(props: SideMenuProps) {
+ return (
+
+
+
+ );
+}
diff --git a/examples/03-ui-components/13-custom-ui/MUISuggestionMenu.tsx b/examples/03-ui-components/13-custom-ui/MUISuggestionMenu.tsx
new file mode 100644
index 0000000000..d677bfe155
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/MUISuggestionMenu.tsx
@@ -0,0 +1,172 @@
+import {
+ DefaultReactSuggestionItem,
+ elementOverflow,
+ SuggestionMenuProps,
+ useBlockNoteEditor,
+} from "@blocknote/react";
+import {
+ Chip,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ ListSubheader,
+ Paper,
+} from "@mui/material";
+import { useEffect, useMemo, useRef } from "react";
+
+import { TextBlockSchema } from "./schema";
+
+// If you want to change the items in a Suggestion Menu, like the Slash Menu,
+// you don't need to modify any of the components in this file. Instead, you
+// should change the array returned in the getItems` prop of the
+// `SuggestionMenuController` in `App.tsx`. The components in this file are only
+// responsible for rendering a Suggestion Menu, not setting its content.
+
+// This replaces the generic Mantine `SuggestionMenuItem` component with a
+// simplified MUI version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/mantine/src/suggestionMenu/SuggestionMenuItem.tsx
+function MUISuggestionMenuItem(
+ props: Omit, "items"> & {
+ item: DefaultReactSuggestionItem & { index: number };
+ }
+) {
+ const Icon = props.item.icon;
+ const editor = useBlockNoteEditor();
+
+ // Scrolls to the item if it's detected to overflow the Slash Menu.
+ const itemRef = useRef(null);
+ useEffect(() => {
+ if (!itemRef.current || props.item.index !== props.selectedIndex) {
+ return;
+ }
+
+ const overflow = elementOverflow(
+ itemRef.current,
+ document.querySelector(
+ `.MuiPaper-root:has([aria-label="suggestion-menu"])`
+ )!
+ );
+
+ if (overflow === "top") {
+ itemRef.current.scrollIntoView(true);
+ } else if (overflow === "bottom") {
+ itemRef.current.scrollIntoView(false);
+ }
+ }, [props.item.index, props.selectedIndex]);
+
+ return (
+ theme.palette.background.paper,
+ }}>
+ {
+ props.onItemClick?.(props.item);
+ editor.focus();
+ }}>
+ {Icon}
+
+ {props.item.badge && }
+
+
+ );
+}
+
+// This replaces the generic Mantine `EmptySuggestionMenuItem` component with a
+// simplified MUI version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/mantine/src/suggestionMenu/EmptySuggestionMenuItem.tsx
+function MUIEmptySuggestionMenuItem() {
+ return (
+
+
+
+
+
+ );
+}
+
+// This replaces the generic Mantine `SuggestionMenuLabel` component with a
+// simplified MUI version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/mantine/src/suggestionMenu/SuggestionMenuLabel.tsx
+function MUISuggestionMenuLabel(props: { group: string }) {
+ return (
+ theme.palette.background.paper,
+ }}>
+ {props.group}
+
+ );
+}
+
+// This replaces the generic Mantine `SuggestionMenu` component with a
+// simplified MUI version:
+// https://github.com/TypeCellOS/BlockNote/blob/main/packages/mantine/src/suggestionMenu/SuggestionMenu.tsx
+export function MUISuggestionMenu(
+ props: SuggestionMenuProps
+) {
+ // Sorts items into their groups.
+ const groups = useMemo(() => {
+ const groups: Record<
+ string,
+ (DefaultReactSuggestionItem & { index: number })[]
+ > = {};
+ for (let i = 0; i < props.items.length; i++) {
+ const item = props.items[i];
+ const group = item.group || item.title;
+
+ if (!groups[group]) {
+ groups[group] = [];
+ }
+
+ groups[group].push({ ...item, index: i });
+ }
+
+ return groups;
+ }, [props.items]);
+
+ return (
+
+
+
+ );
+}
diff --git a/examples/03-ui-components/13-custom-ui/README.md b/examples/03-ui-components/13-custom-ui/README.md
new file mode 100644
index 0000000000..28d0e58e4e
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/README.md
@@ -0,0 +1,11 @@
+# UI With Third-Party Components
+
+In this example, we implement a basic editor interface using components from Material UI. We replace the Formatting Toolbar, Slash Menu, and Block Side Menu while disabling the other default elements. Additionally, the Formatting Toolbar is made static and always visible above the editor.
+
+**Relevant Docs:**
+
+- [Formatting Toolbar](/docs/ui-components/formatting-toolbar)
+- [Manipulating Inline Content](/docs/editor-api/manipulating-inline-content)
+- [Slash Menu](/docs/ui-components/suggestion-menus#slash-menu)
+- [Side Menu](/docs/ui-components/side-menu)
+- [Editor Setup](/docs/editor-basics/setup)
\ No newline at end of file
diff --git a/examples/03-ui-components/13-custom-ui/index.html b/examples/03-ui-components/13-custom-ui/index.html
new file mode 100644
index 0000000000..60fb544419
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ UI With Third-Party Components
+
+
+
+
+
+
diff --git a/examples/03-ui-components/13-custom-ui/main.tsx b/examples/03-ui-components/13-custom-ui/main.tsx
new file mode 100644
index 0000000000..f88b490fbd
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/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/03-ui-components/13-custom-ui/package.json b/examples/03-ui-components/13-custom-ui/package.json
new file mode 100644
index 0000000000..21a3776bf2
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@blocknote/example-custom-ui",
+ "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",
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "@mui/icons-material": "^5.16.1",
+ "@mui/material": "^5.16.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/03-ui-components/13-custom-ui/schema.ts b/examples/03-ui-components/13-custom-ui/schema.ts
new file mode 100644
index 0000000000..1d59d5be57
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/schema.ts
@@ -0,0 +1,14 @@
+import { BlockNoteSchema, defaultBlockSpecs } from "@blocknote/core";
+
+// Simplified schema without media, file, and table blocks.
+export const schema = BlockNoteSchema.create({
+ blockSpecs: {
+ paragraph: defaultBlockSpecs.paragraph,
+ heading: defaultBlockSpecs.heading,
+ bulletListItem: defaultBlockSpecs.bulletListItem,
+ numberedListItem: defaultBlockSpecs.numberedListItem,
+ checkListItem: defaultBlockSpecs.checkListItem,
+ },
+});
+
+export type TextBlockSchema = typeof schema.blockSchema;
diff --git a/examples/03-ui-components/13-custom-ui/style.css b/examples/03-ui-components/13-custom-ui/style.css
new file mode 100644
index 0000000000..c642b08086
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/style.css
@@ -0,0 +1,132 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
+
+.roboto-thin {
+ font-family: "Roboto", sans-serif;
+ font-weight: 100;
+ font-style: normal;
+}
+
+.roboto-light {
+ font-family: "Roboto", sans-serif;
+ font-weight: 300;
+ font-style: normal;
+}
+
+.roboto-regular {
+ font-family: "Roboto", sans-serif;
+ font-weight: 400;
+ font-style: normal;
+}
+
+.roboto-medium {
+ font-family: "Roboto", sans-serif;
+ font-weight: 500;
+ font-style: normal;
+}
+
+.roboto-bold {
+ font-family: "Roboto", sans-serif;
+ font-weight: 700;
+ font-style: normal;
+}
+
+.roboto-black {
+ font-family: "Roboto", sans-serif;
+ font-weight: 900;
+ font-style: normal;
+}
+
+.roboto-thin-italic {
+ font-family: "Roboto", sans-serif;
+ font-weight: 100;
+ font-style: italic;
+}
+
+.roboto-light-italic {
+ font-family: "Roboto", sans-serif;
+ font-weight: 300;
+ font-style: italic;
+}
+
+.roboto-regular-italic {
+ font-family: "Roboto", sans-serif;
+ font-weight: 400;
+ font-style: italic;
+}
+
+.roboto-medium-italic {
+ font-family: "Roboto", sans-serif;
+ font-weight: 500;
+ font-style: italic;
+}
+
+.roboto-bold-italic {
+ font-family: "Roboto", sans-serif;
+ font-weight: 700;
+ font-style: italic;
+}
+
+.roboto-black-italic {
+ font-family: "Roboto", sans-serif;
+ font-weight: 900;
+ font-style: italic;
+}
+
+.text-default {
+ color: var(--bn-colors-editor-text);
+}
+
+.text-red {
+ color: var(--bn-colors-highlights-red-text);
+}
+
+.text-orange {
+ color: var(--bn-colors-highlights-orange-text);
+}
+
+.text-yellow {
+ color: var(--bn-colors-highlights-yellow-text);
+}
+
+.text-green {
+ color: var(--bn-colors-highlights-green-text);
+}
+
+.text-blue {
+ color: var(--bn-colors-highlights-blue-text);
+}
+
+.text-purple {
+ color: var(--bn-colors-highlights-purple-text);
+}
+
+.background-red {
+ background-color: var(--bn-colors-highlights-red-background);
+}
+
+.background-orange {
+ background-color: var(--bn-colors-highlights-orange-background);
+}
+
+.background-yellow {
+ background-color: var(--bn-colors-highlights-yellow-background);
+}
+
+.background-green {
+ background-color: var(--bn-colors-highlights-green-background);
+}
+
+.background-blue {
+ background-color: var(--bn-colors-highlights-blue-background);
+}
+
+.background-purple {
+ background-color: var(--bn-colors-highlights-purple-background);
+}
+
+/* Positions the formatting toolbar above the editor. */
+.bn-container {
+ display: flex;
+ flex-direction: column-reverse;
+ gap: 8px;
+}
diff --git a/examples/03-ui-components/13-custom-ui/tsconfig.json b/examples/03-ui-components/13-custom-ui/tsconfig.json
new file mode 100644
index 0000000000..1bd8ab3c57
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/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/03-ui-components/13-custom-ui/vite.config.ts b/examples/03-ui-components/13-custom-ui/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/13-custom-ui/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/examples/06-custom-schema/04-pdf-file-block/.bnexample.json b/examples/06-custom-schema/04-pdf-file-block/.bnexample.json
index 3045bc5a62..0e8be8663e 100644
--- a/examples/06-custom-schema/04-pdf-file-block/.bnexample.json
+++ b/examples/06-custom-schema/04-pdf-file-block/.bnexample.json
@@ -8,4 +8,4 @@
"react-icons": "^5.2.1"
},
"pro": true
-}
+}
\ No newline at end of file
diff --git a/packages/ariakit/src/suggestionMenu/SuggestionMenuItem.tsx b/packages/ariakit/src/suggestionMenu/SuggestionMenuItem.tsx
index 1169c74031..8acf330c3a 100644
--- a/packages/ariakit/src/suggestionMenu/SuggestionMenuItem.tsx
+++ b/packages/ariakit/src/suggestionMenu/SuggestionMenuItem.tsx
@@ -17,7 +17,10 @@ export const SuggestionMenuItem = forwardRef<
return;
}
- const overflow = elementOverflow(itemRef.current);
+ const overflow = elementOverflow(
+ itemRef.current,
+ document.querySelector(".bn-suggestion-menu")!
+ );
if (overflow === "top") {
itemRef.current.scrollIntoView(true);
diff --git a/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx b/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
index 6db77fda62..75624ede1e 100644
--- a/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
+++ b/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
@@ -17,7 +17,10 @@ export const GridSuggestionMenuItem = forwardRef<
return;
}
- const overflow = elementOverflow(itemRef.current);
+ const overflow = elementOverflow(
+ itemRef.current,
+ document.querySelector(".bn-grid-suggestion-menu")!
+ );
if (overflow === "top") {
itemRef.current.scrollIntoView(true);
diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts
index 440a9b5785..24cdaf8a55 100644
--- a/packages/core/src/editor/BlockNoteEditor.ts
+++ b/packages/core/src/editor/BlockNoteEditor.ts
@@ -753,7 +753,9 @@ export class BlockNoteEditor<
}
return false;
}
- return this._tiptapEditor.isEditable;
+ return this._tiptapEditor.isEditable === undefined
+ ? true
+ : this._tiptapEditor.isEditable;
}
/**
diff --git a/packages/mantine/src/suggestionMenu/SuggestionMenuItem.tsx b/packages/mantine/src/suggestionMenu/SuggestionMenuItem.tsx
index 6174ff4983..c7db27e386 100644
--- a/packages/mantine/src/suggestionMenu/SuggestionMenuItem.tsx
+++ b/packages/mantine/src/suggestionMenu/SuggestionMenuItem.tsx
@@ -25,7 +25,10 @@ export const SuggestionMenuItem = forwardRef<
return;
}
- const overflow = elementOverflow(itemRef.current);
+ const overflow = elementOverflow(
+ itemRef.current,
+ document.querySelector(".bn-suggestion-menu")!
+ );
if (overflow === "top") {
itemRef.current.scrollIntoView(true);
diff --git a/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
index ded689a9cb..a03edfd6ba 100644
--- a/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
+++ b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
@@ -19,7 +19,10 @@ export const GridSuggestionMenuItem = forwardRef<
return;
}
- const overflow = elementOverflow(itemRef.current);
+ const overflow = elementOverflow(
+ itemRef.current,
+ document.querySelector(".bn-grid-suggestion-menu")!
+ );
if (overflow === "top") {
itemRef.current.scrollIntoView(true);
diff --git a/packages/react/src/util/elementOverflow.ts b/packages/react/src/util/elementOverflow.ts
index 25a004a81d..2eff6bf5b4 100644
--- a/packages/react/src/util/elementOverflow.ts
+++ b/packages/react/src/util/elementOverflow.ts
@@ -1,10 +1,6 @@
-export function elementOverflow(element: HTMLElement) {
- if (!element.parentElement) {
- return "none";
- }
-
+export function elementOverflow(element: HTMLElement, container: HTMLElement) {
const elementRect = element.getBoundingClientRect();
- const parentRect = element.parentElement.getBoundingClientRect();
+ const parentRect = container.getBoundingClientRect();
const topOverflow = elementRect.top < parentRect.top;
const bottomOverflow = elementRect.bottom > parentRect.bottom;
diff --git a/packages/shadcn/src/suggestionMenu/SuggestionMenuItem.tsx b/packages/shadcn/src/suggestionMenu/SuggestionMenuItem.tsx
index 162c6b7b94..a8800f3a58 100644
--- a/packages/shadcn/src/suggestionMenu/SuggestionMenuItem.tsx
+++ b/packages/shadcn/src/suggestionMenu/SuggestionMenuItem.tsx
@@ -22,8 +22,10 @@ export const SuggestionMenuItem = forwardRef<
return;
}
- const overflow = elementOverflow(itemRef.current);
-
+ const overflow = elementOverflow(
+ itemRef.current,
+ document.querySelector(".bn-suggestion-menu")!
+ );
if (overflow === "top") {
itemRef.current.scrollIntoView(true);
} else if (overflow === "bottom") {
diff --git a/packages/shadcn/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx b/packages/shadcn/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
index 6db77fda62..75624ede1e 100644
--- a/packages/shadcn/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
+++ b/packages/shadcn/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
@@ -17,7 +17,10 @@ export const GridSuggestionMenuItem = forwardRef<
return;
}
- const overflow = elementOverflow(itemRef.current);
+ const overflow = elementOverflow(
+ itemRef.current,
+ document.querySelector(".bn-grid-suggestion-menu")!
+ );
if (overflow === "top") {
itemRef.current.scrollIntoView(true);
diff --git a/playground/package.json b/playground/package.json
index 2b9e46b373..ecafa565fa 100644
--- a/playground/package.json
+++ b/playground/package.json
@@ -18,9 +18,13 @@
"@blocknote/react": "^0.15.3",
"@blocknote/server-util": "^0.15.3",
"@blocknote/shadcn": "^0.15.3",
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
"@liveblocks/client": "^1.10.0",
"@liveblocks/yjs": "^1.10.0",
"@mantine/core": "^7.10.1",
+ "@mui/icons-material": "^5.16.1",
+ "@mui/material": "^5.16.1",
"@uppy/core": "^3.13.1",
"@uppy/dashboard": "^3.9.1",
"@uppy/drag-drop": "^3.1.1",
diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx
index eaac162755..16f800a1ee 100644
--- a/playground/src/examples.gen.tsx
+++ b/playground/src/examples.gen.tsx
@@ -525,10 +525,31 @@
"slug": "ui-components"
}
},
+ {
+ "projectSlug": "static-formatting-toolbar",
+ "fullSlug": "ui-components/static-formatting-toolbar",
+ "pathFromRoot": "examples/03-ui-components/12-static-formatting-toolbar",
+ "config": {
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": [
+ "Basic",
+ "UI Components",
+ "Formatting Toolbar",
+ "Appearance & Styling"
+ ]
+ },
+ "title": "Static Formatting Toolbar",
+ "group": {
+ "pathFromRoot": "examples/03-ui-components",
+ "slug": "ui-components"
+ }
+ },
{
"projectSlug": "custom-ui",
"fullSlug": "ui-components/custom-ui",
- "pathFromRoot": "examples/03-ui-components/12-custom-ui",
+ "pathFromRoot": "examples/03-ui-components/13-custom-ui",
"config": {
"playground": true,
"docs": true,
@@ -544,11 +565,14 @@
"Appearance & Styling"
],
"dependencies": {
- "react-icons": "^5.2.1"
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "@mui/icons-material": "^5.16.1",
+ "@mui/material": "^5.16.1"
} as any,
"pro": true
},
- "title": "Custom UI",
+ "title": "UI With Third-Party Components",
"group": {
"pathFromRoot": "examples/03-ui-components",
"slug": "ui-components"