diff --git a/examples/03-ui-components/12-custom-ui/App.tsx b/examples/03-ui-components/12-custom-ui/App.tsx deleted file mode 100644 index f3bb61a09f..0000000000 --- a/examples/03-ui-components/12-custom-ui/App.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { filterSuggestionItems } from "@blocknote/core"; -import "@blocknote/core/fonts/inter.css"; -import { - getDefaultReactSlashMenuItems, - SideMenuController, - SuggestionMenuController, - useCreateBlockNote, -} from "@blocknote/react"; -import { BlockNoteView } from "@blocknote/mantine"; -import "@blocknote/mantine/style.css"; - -import { CustomFormattingToolbar } from "./CustomFormattingToolbar"; -import { CustomSideMenu } from "./CustomSideMenu"; -import { CustomSlashMenu } from "./CustomSlashMenu"; -import "./styles.css"; - -export default function App() { - // Creates a new editor instance. - const editor = useCreateBlockNote({ - initialContent: [ - { - type: "paragraph", - content: "Welcome to this demo!", - }, - { - type: "paragraph", - }, - ], - }); - - // Renders the editor instance. - return ( - - {/* Adds the custom Formatting Toolbar */} - {/* `FormattingToolbarController isn't used since the custom toolbar is - static and always visible above the editor. */} - - {/* 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), query) - } - suggestionMenuComponent={CustomSlashMenu} - onItemClick={(i) => i.onItemClick()} - /> - - ); -} diff --git a/examples/03-ui-components/12-custom-ui/ColorMenu.tsx b/examples/03-ui-components/12-custom-ui/ColorMenu.tsx deleted file mode 100644 index 583c84d6a9..0000000000 --- a/examples/03-ui-components/12-custom-ui/ColorMenu.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { - useBlockNoteEditor, - useEditorChange, - useEditorSelectionChange, -} from "@blocknote/react"; -import { useState } from "react"; -import { MdFormatColorText } from "react-icons/md"; - -export const colors = [ - "default", - "red", - "orange", - "yellow", - "green", - "blue", - "purple", -] as const; - -// Formatting Toolbar sub menu for changing text and background color. -export function ColorMenu(props: { className?: string }) { - const editor = useBlockNoteEditor(); - - // Colors of the currently selected text. - const [textColor, setTextColor] = useState( - (editor.getActiveStyles().textColor as string) || "default" - ); - const [backgroundColor, setCurrentColor] = useState( - (editor.getActiveStyles().backgroundColor as string) || "default" - ); - - // Updates the colors when the editor content or selection changes. - useEditorChange(() => { - setTextColor((editor.getActiveStyles().textColor as string) || "default"); - setCurrentColor( - (editor.getActiveStyles().backgroundColor as string) || "default" - ); - }, editor); - useEditorSelectionChange(() => { - setTextColor((editor.getActiveStyles().textColor as string) || "default"); - setCurrentColor( - (editor.getActiveStyles().backgroundColor as string) || "default" - ); - }, editor); - - return ( -
- {/* Group for text color buttons */} -
- {colors.map((color) => ( - // Button for each color - - ))} -
- {/* Group for background color buttons */} -
- {colors.map((color) => ( - // Button for each color - - ))} -
-
- ); -} diff --git a/examples/03-ui-components/12-custom-ui/CustomFormattingToolbar.tsx b/examples/03-ui-components/12-custom-ui/CustomFormattingToolbar.tsx deleted file mode 100644 index 04a4673512..0000000000 --- a/examples/03-ui-components/12-custom-ui/CustomFormattingToolbar.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { - useBlockNoteEditor, - useEditorContentOrSelectionChange, -} from "@blocknote/react"; -import { useState } from "react"; -import { - MdAddLink, - MdFormatAlignCenter, - MdFormatAlignJustify, - MdFormatAlignLeft, - MdFormatAlignRight, - MdFormatBold, - MdFormatColorText, - MdFormatItalic, - MdFormatUnderlined, -} from "react-icons/md"; - -import { checkBlockHasDefaultProp } from "@blocknote/core"; -import { ColorMenu } from "./ColorMenu"; -import { LinkMenu } from "./LinkMenu"; - -type CustomFormattingToolbarState = { - bold: boolean; - italic: boolean; - underline: boolean; - - textAlignment: "left" | "center" | "right" | "justify" | undefined; - - textColor: string; - backgroundColor: string; -}; - -// Custom component to replace the default Formatting Toolbar. -export function CustomFormattingToolbar() { - const editor = useBlockNoteEditor(); - - // Function to get the state of toolbar buttons (active/inactive). - // TODO: this is a bit weird, better to use useSelectedBlocks and useActiveStyles hooks - const getState = (): CustomFormattingToolbarState => { - const block = editor.getTextCursorPosition().block; - const activeStyles = editor.getActiveStyles(); - - return { - bold: (activeStyles.bold as boolean) || false, - italic: (activeStyles.italic as boolean) || false, - underline: (activeStyles.underline as boolean) || false, - - textAlignment: checkBlockHasDefaultProp("textAlignment", block, editor) - ? block.props.textAlignment - : undefined, - - textColor: (activeStyles.textColor as string) || "default", - backgroundColor: (activeStyles.backgroundColor as string) || "default", - }; - }; - - // Callback to set text alignment. - const setTextAlignment = ( - textAlignment: CustomFormattingToolbarState["textAlignment"] - ) => { - const selection = editor.getSelection(); - - if (selection) { - for (const block of selection.blocks) { - editor.updateBlock(block, { - props: { textAlignment: textAlignment }, - }); - } - } else { - const block = editor.getTextCursorPosition().block; - - editor.updateBlock(block, { - props: { textAlignment: textAlignment }, - }); - } - }; - - // Keeps track of the state of toolbar buttons. - const [state, setState] = useState(getState()); - - // Keeps track of if the color and link sub menus are open. - const [colorMenuOpen, setColorMenuOpen] = useState(false); - const [linkMenuOpen, setLinkMenuOpen] = useState(false); - - // Updates toolbar state when the editor content or selection changes. - useEditorContentOrSelectionChange(() => setState(getState()), editor); - - return ( -
- {/* Button group for toggled text styles. */} -
- {/* Toggle bold button */} - - {/* Toggle italic button */} - - {/* Toggle underline button */} - -
- {/* Button group for text alignment */} - {state.textAlignment && ( -
- {/*Left align button*/} - - {/* Center align button */} - - {/* Right align button */} - - {/* Justify text button */} - -
- )} - {/* Button group for color menu */} -
-
- - -
-
- {/* Button group for link menu */} -
-
- - -
-
-
- ); -} diff --git a/examples/03-ui-components/12-custom-ui/CustomSideMenu.tsx b/examples/03-ui-components/12-custom-ui/CustomSideMenu.tsx deleted file mode 100644 index 85ec869f5c..0000000000 --- a/examples/03-ui-components/12-custom-ui/CustomSideMenu.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { SideMenuProps } from "@blocknote/react"; -import { RxDragHandleHorizontal } from "react-icons/rx"; - -// Custom component to replace the default Block Side Menu. -export function CustomSideMenu(props: SideMenuProps) { - return ( -
- -
- ); -} diff --git a/examples/03-ui-components/12-custom-ui/CustomSlashMenu.tsx b/examples/03-ui-components/12-custom-ui/CustomSlashMenu.tsx deleted file mode 100644 index e4061ef8bf..0000000000 --- a/examples/03-ui-components/12-custom-ui/CustomSlashMenu.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { - DefaultReactSuggestionItem, - SuggestionMenuProps, - useBlockNoteEditor, -} from "@blocknote/react"; - -// Custom component to replace the default Slash Menu. -export function CustomSlashMenu( - props: SuggestionMenuProps -) { - const editor = useBlockNoteEditor(); - - // Sorts items into their groups. - const groups: Record = {}; - for (const item of props.items) { - const group = item.group || item.title; - - if (!groups[group]) { - groups[group] = []; - } - - groups[group].push(item); - } - - // If query matches no items, shows "No matches" message. - if (props.items.length === 0) { - return
No matches
; - } - - return ( -
- {Object.entries(groups).map(([group, items]) => ( - // Component for each group -
- {/* Group label */} -
{group}
- {/* Group items */} -
- {items.map((item: DefaultReactSuggestionItem) => { - const Icon = item.icon; - return ( - - ); - })} -
-
- ))} -
- ); -} diff --git a/examples/03-ui-components/12-custom-ui/LinkMenu.tsx b/examples/03-ui-components/12-custom-ui/LinkMenu.tsx deleted file mode 100644 index 5f0631b96d..0000000000 --- a/examples/03-ui-components/12-custom-ui/LinkMenu.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { HTMLAttributes, useState } from "react"; -import { BlockNoteEditor } from "@blocknote/core"; - -// Formatting Toolbar sub menu for creating links. -export const LinkMenu = ( - props: { editor: BlockNoteEditor } & HTMLAttributes -) => { - const { editor, className, ...rest } = props; - - const [text, setText] = useState(""); - const [url, setUrl] = useState(""); - - return ( -
- {/*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 ( + <> + + + + + + Text Color + + {colors.map((color) => ( + textColorOnClick(color)}> + + + + + + {color.slice(0, 1).toUpperCase() + color.slice(1)} + + + {color === activeTextColor && ( + + )} + + ))} + + + Background Color + + {colors.map((color) => ( + backgroundColorOnClick(color)}> + + + + + + {color.slice(0, 1).toUpperCase() + color.slice(1)} + + + {color === activeBackgroundColor && ( + + )} + + ))} + + + ); +} + +// 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 ( + + + theme.palette.text.primary, + padding: "0.1em", + height: "0.8em", + width: "0.8em", + }} + /> + + + Delete Block + + + ); +} + +// 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 ( + + {props.children} + + ); +} + +// 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"