diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index 4193271928..c0a0e3aedf 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -99,6 +99,7 @@ export const getBlockNoteExtensions = (opts: { if (opts.uiFactories.blockSideMenuFactory) { ret.push( DraggableBlocksExtension.configure({ + editor: opts.editor, blockSideMenuFactory: opts.uiFactories.blockSideMenuFactory, }) ); diff --git a/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts b/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts index fed0df3068..b376feacc5 100644 --- a/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +++ b/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts @@ -1,22 +1,21 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement"; +import { BlockNoteEditor } from "../../BlockNoteEditor"; +import { Block } from "../Blocks/api/blockTypes"; export type BlockSideMenuStaticParams = { + editor: BlockNoteEditor; + addBlock: () => void; - deleteBlock: () => void; blockDragStart: (event: DragEvent) => void; blockDragEnd: () => void; freezeMenu: () => void; unfreezeMenu: () => void; - - setBlockTextColor: (color: string) => void; - setBlockBackgroundColor: (color: string) => void; }; export type BlockSideMenuDynamicParams = { - blockTextColor: string; - blockBackgroundColor: string; + block: Block; referenceRect: DOMRect; }; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts index 8f4a7af603..9156368eed 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts @@ -1,9 +1,11 @@ import { Editor, Extension } from "@tiptap/core"; import { BlockSideMenuFactory } from "./BlockSideMenuFactoryTypes"; import { createDraggableBlocksPlugin } from "./DraggableBlocksPlugin"; +import { BlockNoteEditor } from "../../BlockNoteEditor"; export type DraggableBlocksOptions = { - editor: Editor; + tiptapEditor: Editor; + editor: BlockNoteEditor; blockSideMenuFactory: BlockSideMenuFactory; }; @@ -25,7 +27,8 @@ export const DraggableBlocksExtension = } return [ createDraggableBlocksPlugin({ - editor: this.editor, + tiptapEditor: this.editor, + editor: this.options.editor, blockSideMenuFactory: this.options.blockSideMenuFactory, }), ]; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index 0e1fd655a8..9cb42464ec 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -14,26 +14,13 @@ import { } from "./BlockSideMenuFactoryTypes"; import { DraggableBlocksOptions } from "./DraggableBlocksExtension"; import { MultipleNodeSelection } from "./MultipleNodeSelection"; +import { BlockNoteEditor } from "../../BlockNoteEditor"; const serializeForClipboard = (pv as any).__serializeForClipboard; // code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799 let dragImageElement: Element | undefined; -export function createRect(rect: DOMRect) { - let newRect = { - left: rect.left + document.body.scrollLeft, - top: rect.top + document.body.scrollTop, - width: rect.width, - height: rect.height, - bottom: 0, - right: 0, - }; - newRect.bottom = newRect.top + newRect.height; - newRect.right = newRect.left + newRect.width; - return newRect; -} - function getDraggableBlockFromCoords( coords: { left: number; top: number }, view: EditorView @@ -237,13 +224,15 @@ function dragStart(e: DragEvent, view: EditorView) { } export type BlockMenuViewProps = { - editor: Editor; + tiptapEditor: Editor; + editor: BlockNoteEditor; blockMenuFactory: BlockSideMenuFactory; horizontalPosAnchoredAtRoot: boolean; }; export class BlockMenuView { - editor: Editor; + editor: BlockNoteEditor; + private ttEditor: Editor; // When true, the drag handle with be anchored at the same level as root elements // When false, the drag handle with be just to the left of the element @@ -253,20 +242,22 @@ export class BlockMenuView { blockMenu: BlockSideMenu; - hoveredBlockContent: HTMLElement | undefined; + hoveredBlock: HTMLElement | undefined; menuOpen = false; menuFrozen = false; constructor({ + tiptapEditor, editor, blockMenuFactory, horizontalPosAnchoredAtRoot, }: BlockMenuViewProps) { this.editor = editor; + this.ttEditor = tiptapEditor; this.horizontalPosAnchoredAtRoot = horizontalPosAnchoredAtRoot; this.horizontalPosAnchor = ( - editor.view.dom.firstChild! as HTMLElement + this.ttEditor.view.dom.firstChild! as HTMLElement ).getBoundingClientRect().x; this.blockMenu = blockMenuFactory(this.getStaticParams()); @@ -287,7 +278,7 @@ export class BlockMenuView { } /** - * If the event is outside of the editor contents, + * If the event is outside the editor contents, * we dispatch a fake event, so that we can still drop the content * when dragging / dropping to the side of the editor */ @@ -295,7 +286,7 @@ export class BlockMenuView { if ((event as any).synthetic) { return; } - let pos = this.editor.view.posAtCoords({ + let pos = this.ttEditor.view.posAtCoords({ left: event.clientX, top: event.clientY, }); @@ -303,7 +294,7 @@ export class BlockMenuView { if (!pos || pos.inside === -1) { const evt = new Event("drop", event) as any; const editorBoundingBox = ( - this.editor.view.dom.firstChild! as HTMLElement + this.ttEditor.view.dom.firstChild! as HTMLElement ).getBoundingClientRect(); evt.clientX = editorBoundingBox.left + editorBoundingBox.width / 2; evt.clientY = event.clientY; @@ -311,7 +302,7 @@ export class BlockMenuView { evt.preventDefault = () => event.preventDefault(); evt.synthetic = true; // prevent recursion // console.log("dispatch fake drop"); - this.editor.view.dom.dispatchEvent(evt); + this.ttEditor.view.dom.dispatchEvent(evt); } }; @@ -324,7 +315,7 @@ export class BlockMenuView { if ((event as any).synthetic) { return; } - let pos = this.editor.view.posAtCoords({ + let pos = this.ttEditor.view.posAtCoords({ left: event.clientX, top: event.clientY, }); @@ -332,7 +323,7 @@ export class BlockMenuView { if (!pos || pos.inside === -1) { const evt = new Event("dragover", event) as any; const editorBoundingBox = ( - this.editor.view.dom.firstChild! as HTMLElement + this.ttEditor.view.dom.firstChild! as HTMLElement ).getBoundingClientRect(); evt.clientX = editorBoundingBox.left + editorBoundingBox.width / 2; evt.clientY = event.clientY; @@ -340,7 +331,7 @@ export class BlockMenuView { evt.preventDefault = () => event.preventDefault(); evt.synthetic = true; // prevent recursion // console.log("dispatch fake dragover"); - this.editor.view.dom.dispatchEvent(evt); + this.ttEditor.view.dom.dispatchEvent(evt); } }; @@ -374,7 +365,7 @@ export class BlockMenuView { // Editor itself may have padding or other styling which affects size/position, so we get the boundingRect of // the first child (i.e. the blockGroup that wraps all blocks in the editor) for a more accurate bounding box. const editorBoundingBox = ( - this.editor.view.dom.firstChild! as HTMLElement + this.ttEditor.view.dom.firstChild! as HTMLElement ).getBoundingClientRect(); this.horizontalPosAnchor = editorBoundingBox.x; @@ -384,7 +375,7 @@ export class BlockMenuView { left: editorBoundingBox.left + editorBoundingBox.width / 2, // take middle of editor top: event.clientY, }; - const block = getDraggableBlockFromCoords(coords, this.editor.view); + const block = getDraggableBlockFromCoords(coords, this.ttEditor.view); // Closes the menu if the mouse cursor is beyond the editor vertically. if (!block || !this.editor.isEditable) { @@ -399,15 +390,16 @@ export class BlockMenuView { // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block. if ( this.menuOpen && - this.hoveredBlockContent?.hasAttribute("data-id") && - this.hoveredBlockContent?.getAttribute("data-id") === block.id + this.hoveredBlock?.hasAttribute("data-id") && + this.hoveredBlock?.getAttribute("data-id") === block.id ) { return; } + this.hoveredBlock = block.node; + // Gets the block's content node, which lets to ignore child blocks when determining the block menu's position. const blockContent = block.node.firstChild as HTMLElement; - this.hoveredBlockContent = blockContent; if (!blockContent) { return; @@ -448,10 +440,10 @@ export class BlockMenuView { this.menuFrozen = true; this.blockMenu.hide(); - const blockContentBoundingBox = - this.hoveredBlockContent!.getBoundingClientRect(); + const blockContent = this.hoveredBlock!.firstChild! as HTMLElement; + const blockContentBoundingBox = blockContent.getBoundingClientRect(); - const pos = this.editor.view.posAtCoords({ + const pos = this.ttEditor.view.posAtCoords({ left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2, top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2, }); @@ -459,7 +451,7 @@ export class BlockMenuView { return; } - const blockInfo = getBlockInfoFromPos(this.editor.state.doc, pos.pos); + const blockInfo = getBlockInfoFromPos(this.ttEditor.state.doc, pos.pos); if (blockInfo === undefined) { return; } @@ -471,20 +463,20 @@ export class BlockMenuView { const newBlockInsertionPos = endPos + 1; const newBlockContentPos = newBlockInsertionPos + 2; - this.editor + this.ttEditor .chain() .BNCreateBlock(newBlockInsertionPos) .BNUpdateBlock(newBlockContentPos, { type: "paragraph", props: {} }) .setTextSelection(newBlockContentPos) .run(); } else { - this.editor.commands.setTextSelection(endPos); + this.ttEditor.commands.setTextSelection(endPos); } // Focuses and activates the suggestion menu. - this.editor.view.focus(); - this.editor.view.dispatch( - this.editor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, { + this.ttEditor.view.focus(); + this.ttEditor.view.dispatch( + this.ttEditor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, { // TODO import suggestion plugin key activate: true, type: "drag", @@ -492,65 +484,12 @@ export class BlockMenuView { ); } - deleteBlock() { - this.menuOpen = false; - this.blockMenu.hide(); - - const blockContentBoundingBox = - this.hoveredBlockContent!.getBoundingClientRect(); - - const pos = this.editor.view.posAtCoords({ - left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2, - top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2, - }); - if (!pos) { - return; - } - - this.editor.commands.BNDeleteBlock(pos.pos); - } - - setBlockBackgroundColor(color: string) { - this.menuOpen = false; - this.blockMenu.hide(); - - const blockContentBoundingBox = - this.hoveredBlockContent!.getBoundingClientRect(); - - const pos = this.editor.view.posAtCoords({ - left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2, - top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2, - }); - if (!pos) { - return; - } - - this.editor.commands.setBlockBackgroundColor(pos.pos, color); - } - - setBlockTextColor(color: string) { - this.menuOpen = false; - this.blockMenu.hide(); - - const blockContentBoundingBox = - this.hoveredBlockContent!.getBoundingClientRect(); - - const pos = this.editor.view.posAtCoords({ - left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2, - top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2, - }); - if (!pos) { - return; - } - - this.editor.commands.setBlockTextColor(pos.pos, color); - } - getStaticParams(): BlockSideMenuStaticParams { return { + editor: this.editor, addBlock: () => this.addBlock(), - deleteBlock: () => this.deleteBlock(), - blockDragStart: (event: DragEvent) => dragStart(event, this.editor.view), + blockDragStart: (event: DragEvent) => + dragStart(event, this.ttEditor.view), blockDragEnd: () => unsetDragImage(), freezeMenu: () => { this.menuFrozen = true; @@ -558,20 +497,15 @@ export class BlockMenuView { unfreezeMenu: () => { this.menuFrozen = false; }, - setBlockBackgroundColor: (color: string) => - this.setBlockBackgroundColor(color), - setBlockTextColor: (color: string) => this.setBlockTextColor(color), }; } getDynamicParams(): BlockSideMenuDynamicParams { - const blockContentBoundingBox = - this.hoveredBlockContent!.getBoundingClientRect(); + const blockContent = this.hoveredBlock!.firstChild! as HTMLElement; + const blockContentBoundingBox = blockContent.getBoundingClientRect(); return { - blockBackgroundColor: - this.editor.getAttributes("blockContainer").backgroundColor, - blockTextColor: this.editor.getAttributes("blockContainer").textColor, + block: this.editor.getBlock(this.hoveredBlock!.getAttribute("data-id")!)!, referenceRect: new DOMRect( this.horizontalPosAnchoredAtRoot ? this.horizontalPosAnchor @@ -591,6 +525,7 @@ export const createDraggableBlocksPlugin = ( key: new PluginKey("DraggableBlocksPlugin"), view: () => new BlockMenuView({ + tiptapEditor: options.tiptapEditor, editor: options.editor, blockMenuFactory: options.blockSideMenuFactory, horizontalPosAnchoredAtRoot: true, diff --git a/packages/react/package.json b/packages/react/package.json index caef8d5c83..1b73655115 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -50,6 +50,7 @@ "@emotion/react": "^11.10.5", "@mantine/core": "^5.6.1", "@mantine/hooks": "^5.6.1", + "@mantine/utils": "^6.0.5", "@tippyjs/react": "^4.2.6", "@tiptap/react": "2.0.0-beta.217", "react-icons": "^4.3.1" diff --git a/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx index 94cfdb179d..fedc81ff58 100644 --- a/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx +++ b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx @@ -1,21 +1,29 @@ +import { FC } from "react"; import { - BlockSideMenu, BlockSideMenuDynamicParams, - BlockSideMenuFactory, BlockSideMenuStaticParams, } from "@blocknote/core"; import { BlockSideMenu as ReactBlockSideMenu } from "./components/BlockSideMenu"; import { ReactElementFactory } from "../ElementFactory/components/ReactElementFactory"; +import { DragHandleMenuProps } from "./components/DragHandleMenu"; +import { DefaultDragHandleMenu } from "./components/DefaultDragHandleMenu"; -export const ReactBlockSideMenuFactory: BlockSideMenuFactory = ( - staticParams -): BlockSideMenu => - ReactElementFactory( - staticParams, - ReactBlockSideMenu, - { - animation: "fade", - offset: [0, 0], - placement: "left", - } - ); +export const createReactBlockSideMenuFactory = ( + dragHandleMenu: FC = DefaultDragHandleMenu +) => { + const CustomDragHandleMenu = dragHandleMenu; + const CustomBlockSideMenu = ( + props: BlockSideMenuStaticParams & BlockSideMenuDynamicParams + ) => ; + + return (staticParams: BlockSideMenuStaticParams) => + ReactElementFactory( + staticParams, + CustomBlockSideMenu, + { + animation: "fade", + offset: [0, 0], + placement: "left", + } + ); +}; diff --git a/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx b/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx index ecdf2fef69..f828e5f696 100644 --- a/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx +++ b/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx @@ -1,27 +1,22 @@ +import { FC, useEffect, useRef, useState } from "react"; +import { ActionIcon, Group, Menu } from "@mantine/core"; +import { Block, BlockNoteEditor } from "@blocknote/core"; import { AiOutlinePlus, MdDragIndicator } from "react-icons/all"; -import { ActionIcon, createStyles, Group, Menu } from "@mantine/core"; -import { useEffect, useRef, useState } from "react"; -import { ColorPickerMenu } from "./ColorPickerMenu"; +import { DragHandleMenuProps } from "./DragHandleMenu"; +import { DefaultDragHandleMenu } from "./DefaultDragHandleMenu"; export type BlockSideMenuProps = { + editor: BlockNoteEditor; + block: Block; + dragHandleMenu?: FC; addBlock: () => void; - deleteBlock: () => void; blockDragStart: (event: DragEvent) => void; blockDragEnd: () => void; freezeMenu: () => void; unfreezeMenu: () => void; - - blockBackgroundColor: string; - setBlockBackgroundColor: (color: string) => void; - blockTextColor: string; - setBlockTextColor: (color: string) => void; }; export const BlockSideMenu = (props: BlockSideMenuProps) => { - const { classes } = createStyles({ root: {} })(undefined, { - name: "DragHandleMenu", - }); - const [dragHandleMenuOpened, setDragHandleMenuOpened] = useState(false); const dragHandleRef = useRef(null); @@ -42,6 +37,13 @@ export const BlockSideMenu = (props: BlockSideMenuProps) => { return; }, [props.blockDragEnd, props.blockDragStart]); + const closeMenu = () => { + setDragHandleMenuOpened(false); + props.unfreezeMenu(); + }; + + const DragHandleMenu = props.dragHandleMenu || DefaultDragHandleMenu; + return ( @@ -69,24 +71,11 @@ export const BlockSideMenu = (props: BlockSideMenuProps) => { - - { - setDragHandleMenuOpened(false); - props.unfreezeMenu(); - props.deleteBlock(); - }}> - Delete - - { - setDragHandleMenuOpened(false); - props.unfreezeMenu(); - }} - {...props} - /> - + ); diff --git a/packages/react/src/BlockSideMenu/components/ColorPickerMenu.tsx b/packages/react/src/BlockSideMenu/components/ColorPickerMenu.tsx deleted file mode 100644 index d936b86545..0000000000 --- a/packages/react/src/BlockSideMenu/components/ColorPickerMenu.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Box, Menu } from "@mantine/core"; -import { HiChevronRight } from "react-icons/hi"; -import { ColorPicker } from "../../SharedComponents/ColorPicker/components/ColorPicker"; -import { useCallback, useState } from "react"; - -export const ColorPickerMenu = (props: { - onClick?: () => void; - - blockBackgroundColor: string; - setBlockBackgroundColor: (color: string) => void; - blockTextColor: string; - setBlockTextColor: (color: string) => void; -}) => { - const [opened, setOpened] = useState(false); - const [menuCloseTimer, setMenuCloseTimer] = useState< - NodeJS.Timeout | undefined - >(undefined); - const [buttonBackground, setButtonBackground] = useState( - undefined - ); - - const startMenuCloseTimer = useCallback(() => { - setMenuCloseTimer( - setTimeout(() => { - setOpened(false); - }, 250) - ); - }, []); - - const stopMenuCloseTimer = useCallback(() => { - if (menuCloseTimer) { - clearTimeout(menuCloseTimer); - setMenuCloseTimer(undefined); - } - setOpened(true); - }, [menuCloseTimer]); - - return ( - - { - startMenuCloseTimer(); - setButtonBackground(undefined); - }} - onMouseOver={() => { - stopMenuCloseTimer(); - setButtonBackground("#f1f3f5"); - }} - rightSection={ - - - - }> - Colors - - { - startMenuCloseTimer(); - setButtonBackground(undefined); - }} - onMouseOver={() => { - stopMenuCloseTimer(); - setButtonBackground("#f1f3f5"); - }} - style={{ marginLeft: "90px" }}> - - - - ); -}; diff --git a/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx b/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx new file mode 100644 index 0000000000..ed748ab58c --- /dev/null +++ b/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx @@ -0,0 +1,68 @@ +import { ReactNode, useCallback, useRef, useState } from "react"; +import { Box, Menu } from "@mantine/core"; +import { HiChevronRight } from "react-icons/hi"; +import { DragHandleMenuProps } from "../DragHandleMenu"; +import { DragHandleMenuItem } from "../DragHandleMenuItem"; +import { ColorPicker } from "../../../SharedComponents/ColorPicker/components/ColorPicker"; + +export const BlockColorsButton = ( + props: DragHandleMenuProps & { children: ReactNode } +) => { + const [opened, setOpened] = useState(false); + + const menuCloseTimer = useRef(); + + const startMenuCloseTimer = useCallback(() => { + if (menuCloseTimer.current) { + clearTimeout(menuCloseTimer.current); + } + menuCloseTimer.current = setTimeout(() => { + setOpened(false); + }, 250); + }, []); + + const stopMenuCloseTimer = useCallback(() => { + if (menuCloseTimer.current) { + clearTimeout(menuCloseTimer.current); + } + setOpened(true); + }, []); + + return ( + + + +
+
{props.children}
+ + + +
+
+ + + props.editor.updateBlock(props.block, { + props: { textColor: color }, + }) + } + setBackgroundColor={(color) => + props.editor.updateBlock(props.block, { + props: { backgroundColor: color }, + }) + } + /> + +
+
+ ); +}; diff --git a/packages/react/src/BlockSideMenu/components/DefaultButtons/RemoveBlockButton.tsx b/packages/react/src/BlockSideMenu/components/DefaultButtons/RemoveBlockButton.tsx new file mode 100644 index 0000000000..7111a8b71f --- /dev/null +++ b/packages/react/src/BlockSideMenu/components/DefaultButtons/RemoveBlockButton.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from "react"; + +import { DragHandleMenuProps } from "../DragHandleMenu"; +import { DragHandleMenuItem } from "../DragHandleMenuItem"; + +export const RemoveBlockButton = ( + props: DragHandleMenuProps & { children: ReactNode } +) => { + return ( + props.editor.removeBlocks([props.block])}> + {props.children} + + ); +}; diff --git a/packages/react/src/BlockSideMenu/components/DefaultDragHandleMenu.tsx b/packages/react/src/BlockSideMenu/components/DefaultDragHandleMenu.tsx new file mode 100644 index 0000000000..8f11a29afe --- /dev/null +++ b/packages/react/src/BlockSideMenu/components/DefaultDragHandleMenu.tsx @@ -0,0 +1,10 @@ +import { DragHandleMenu, DragHandleMenuProps } from "./DragHandleMenu"; +import { RemoveBlockButton } from "./DefaultButtons/RemoveBlockButton"; +import { BlockColorsButton } from "./DefaultButtons/BlockColorsButton"; + +export const DefaultDragHandleMenu = (props: DragHandleMenuProps) => ( + + Delete + Colors + +); diff --git a/packages/react/src/BlockSideMenu/components/DragHandleMenu.tsx b/packages/react/src/BlockSideMenu/components/DragHandleMenu.tsx new file mode 100644 index 0000000000..6d7e227dfc --- /dev/null +++ b/packages/react/src/BlockSideMenu/components/DragHandleMenu.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from "react"; +import { createStyles, Menu } from "@mantine/core"; +import { Block, BlockNoteEditor } from "@blocknote/core"; + +export type DragHandleMenuProps = { + editor: BlockNoteEditor; + block: Block; + closeMenu: () => void; +}; + +export const DragHandleMenu = (props: { children: ReactNode }) => { + const { classes } = createStyles({ root: {} })(undefined, { + name: "DragHandleMenu", + }); + + return ( + {props.children} + ); +}; diff --git a/packages/react/src/BlockSideMenu/components/DragHandleMenuItem.tsx b/packages/react/src/BlockSideMenu/components/DragHandleMenuItem.tsx new file mode 100644 index 0000000000..b99774fb4a --- /dev/null +++ b/packages/react/src/BlockSideMenu/components/DragHandleMenuItem.tsx @@ -0,0 +1,17 @@ +import { Menu } from "@mantine/core"; +import { PolymorphicComponentProps } from "@mantine/utils"; + +export type DragHandleMenuItemProps = PolymorphicComponentProps<"button"> & { + closeMenu: () => void; +}; + +export const DragHandleMenuItem = (props: DragHandleMenuItemProps) => ( + { + props.closeMenu(); + props.onClick?.(event); + }}> + {props.children} + +); diff --git a/packages/react/src/hooks/useBlockNote.ts b/packages/react/src/hooks/useBlockNote.ts index 865d82c536..802cfddd45 100644 --- a/packages/react/src/hooks/useBlockNote.ts +++ b/packages/react/src/hooks/useBlockNote.ts @@ -1,6 +1,8 @@ import { BlockNoteEditor, BlockNoteEditorOptions } from "@blocknote/core"; import { DependencyList, useEffect, useState } from "react"; -import { ReactBlockSideMenuFactory } from "../BlockSideMenu/BlockSideMenuFactory"; +import { + createReactBlockSideMenuFactory, +} from "../BlockSideMenu/BlockSideMenuFactory"; import { createReactFormattingToolbarFactory, } from "../FormattingToolbar/FormattingToolbarFactory"; @@ -42,7 +44,7 @@ export const useBlockNote = ( formattingToolbarFactory: createReactFormattingToolbarFactory(), hyperlinkToolbarFactory: ReactHyperlinkToolbarFactory, slashMenuFactory: ReactSlashMenuFactory, - blockSideMenuFactory: ReactBlockSideMenuFactory, + blockSideMenuFactory: createReactBlockSideMenuFactory(), }, }; } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 93e02ec8b3..c29a62000a 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,6 +2,10 @@ export * from "./BlockNoteView"; export * from "./BlockSideMenu/BlockSideMenuFactory"; +export * from "./BlockSideMenu/components/DragHandleMenu"; +export * from "./BlockSideMenu/components/DragHandleMenuItem"; +export * from "./BlockSideMenu/components/DefaultButtons/RemoveBlockButton"; +export * from "./BlockSideMenu/components/DefaultButtons/BlockColorsButton"; export * from "./FormattingToolbar/FormattingToolbarFactory"; export * from "./FormattingToolbar/components/FormattingToolbar"; diff --git a/packages/website/docs/docs/side-menu.md b/packages/website/docs/docs/side-menu.md index 7b6e4e22b3..640a4e0e8b 100644 --- a/packages/website/docs/docs/side-menu.md +++ b/packages/website/docs/docs/side-menu.md @@ -1,8 +1,133 @@ -# Side Menu +# Block Side Menu -Coming soon! +The Block Side Menu appears whenever you hover over a block, and is used to drag & drop the block as well as add new ones below it. - +image + +You can also click the drag handle in the Block Side Menu (`⠿`) to open the Drag Handle Menu. + +image + +## Custom Drag Handle Menu + +BlockNote lets you customize which items appear in the Drag Handle Menu. Have a look at the example below, in which the color picker item is replaced with a custom item that opens an alert. + +::: sandbox {template=react-ts} + +```typescript /App.tsx +import { Block, BlockNoteEditor } from "@blocknote/core"; +import { + BlockNoteView, + createReactBlockSideMenuFactory, + DragHandleMenu, + DragHandleMenuItem, + RemoveBlockButton, + useBlockNote, +} from "@blocknote/react"; +import "@blocknote/core/style.css"; + +const CustomDragHandleMenu = (props: { + editor: BlockNoteEditor; + block: Block; + closeMenu: () => void; +}) => { + return ( + + {/*Default button to remove the block.*/} + + Delete + + {/*Custom item which opens an alert when clicked.*/} + { + window.alert("Button Pressed!"); + props.closeMenu(); + }}> + Open Alert + + + ); +}; + +export default function App() { + // Creates a new editor instance. + const editor: BlockNoteEditor = useBlockNote({ + uiFactories: { + // Makes the editor instance use the custom menu. + blockSideMenuFactory: + createReactBlockSideMenuFactory(CustomDragHandleMenu), + }, + }); + // Renders the editor instance. + return ; +} +``` + +::: + +Let's look at how this is done. We first need to create a custom Drag Handle Menu using a React component. This component should take the following props: + +```typescript +type CustomDragHandleMenuProps = { + editor: BlockNoteEditor; + block: Block; + closeMenu: () => void; +}; +const CustomDragHandleMenu = (props: CustomDragHandleMenuProps): JSX.Element => ...; +``` + +You can then tell BlockNote to use your custom Drag Handle Menu using the `uiFactories` option in `useBlockNote`, but you first have to turn it into a `SideMenuFactory` that uses it. Fortunately, you can easily do this using the `createReactSideMenuFactory` function: + +```typescript +const editor = useBlockNote({ + uiFactories: { + blockSideMenuFactory: createReactBlockSideMenuFactory( + CustomBlockSideMenu + ), + }, +}); +``` + +## Default Items + +It might seem daunting to create your own Drag Handle Menu from scratch, which is why BlockNote provides React components for everything you see in the default layout - both the menu itself and the items in it. Below are all the default components you can use to build your custom menu: + +```typescript +// Menu which wraps all the items. +type BlockSideMenuProps = { + children: ReactNode +} +const BlockSideMenu = (props: BlockSideMenuProps) => ...; + +// Button which removes the block. +type RemoveBlockButtonProps = { + editor: BlockNoteEditor; + block: Block; + closeMenu: () => void; + children: ReactNode; +}; +const RemoveBlockButton = (props: RemoveBlockButtonProps) => ...; + +// Button which opens a dropdown on hover. The dropdown lets you set the block's color. +type BlockColorsButtonProps = { + editor: BlockNoteEditor; + block: Block; + closeMenu: () => void; + children: ReactNode; +}; +const BlockColorsButton = (props: BlockColorsButtonProps) => ...; +``` + +## Custom Items + +BlockNote also provides components that you can use to make your own menu items, which match BlockNote's UI styling: + +```typescript +// Also includes all props of button elements, e.g. onClick. +type DragHandleMenuItemProps = { + // Closes the menu when called. + closeMenu: () => void; +}; +export const DragHandleMenuItem = (props: DragHandleMenuItemProps) => ...; +``` \ No newline at end of file diff --git a/packages/website/docs/public/img/screenshots/drag_handle_menu.png b/packages/website/docs/public/img/screenshots/drag_handle_menu.png new file mode 100644 index 0000000000..7ee2887b94 Binary files /dev/null and b/packages/website/docs/public/img/screenshots/drag_handle_menu.png differ diff --git a/packages/website/docs/public/img/screenshots/side_menu.png b/packages/website/docs/public/img/screenshots/side_menu.png new file mode 100644 index 0000000000..9b5687bc8b Binary files /dev/null and b/packages/website/docs/public/img/screenshots/side_menu.png differ