From 242d42bcb8e8bbb96060a3fc9b98b3d24044feb0 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 13 Feb 2024 23:35:55 +0100 Subject: [PATCH 1/7] Refactored how remaining UI elements (except table handles) are created to match suggestions --- .../examples/react-custom-styles/App.tsx | 4 +- .../examples/vanilla-custom-styles/App.tsx | 4 +- .../vanilla/src/ui/addFormattingToolbar.ts | 45 +++--- .../vanilla/src/ui/addHyperlinkToolbar.ts | 61 ++++---- examples/vanilla/src/ui/addSideMenu.ts | 49 +++--- examples/vanilla/src/ui/addSlashMenu.ts | 148 +++++++++--------- .../extensions-shared/BaseUiElementTypes.ts | 8 - .../extensions-shared/UiElementPosition.ts | 4 + .../FormattingToolbarPlugin.ts | 65 ++++---- .../HyperlinkToolbarPlugin.ts | 97 +++++++----- .../ImageToolbar/ImageToolbarPlugin.ts | 96 +++++++----- .../src/extensions/SideMenu/SideMenuPlugin.ts | 104 +++++++----- .../SuggestionMenu/SuggestionPlugin.ts | 92 ++++++++--- packages/core/src/index.ts | 2 +- .../src/components-shared/UiComponentTypes.ts | 17 ++ .../DefaultButtons/ReplaceImageButton.tsx | 5 +- .../DefaultFormattingToolbar.tsx | 30 ++-- .../DefaultPositionedFormattingToolbar.tsx | 30 ++++ .../useFormattingToolbarPosition.ts} | 70 ++++----- .../DefaultHyperlinkToolbar.tsx | 39 +++-- .../DefaultPositionedHyperlinkToolbar.tsx | 36 +++++ .../HyperlinkToolbarPositioner.tsx | 96 ------------ .../hooks/useHyperlinkToolbarData.ts | 39 +++++ .../hooks/useHyperlinkToolbarPosition.ts | 59 +++++++ .../ImageToolbar/DefaultImageToolbar.tsx | 26 +-- .../DefaultPositionedImageToolbar.tsx | 33 ++++ .../ImageToolbar/ImageToolbarPositioner.tsx | 78 --------- .../ImageToolbar/hooks/useImageToolbarData.ts | 33 ++++ .../hooks/useImageToolbarPosition.ts | 59 +++++++ .../DefaultButtons/AddBlockButton.tsx | 6 +- .../SideMenu/DefaultButtons/DragHandle.tsx | 20 ++- .../SideMenu/DefaultPositionedSideMenu.tsx | 38 +++++ .../components/SideMenu/DefaultSideMenu.tsx | 46 ++++-- .../SideMenu/SideMenuPositioner.tsx | 89 ----------- .../SideMenu/hooks/useSideMenuData.ts | 38 +++++ .../SideMenu/hooks/useSideMenuPosition.ts | 53 +++++++ .../DefaultPositionedSuggestionMenu.tsx | 52 ++++++ .../SuggestionMenu/DefaultSuggestionMenu.tsx | 77 ++------- .../MantineSuggestionMenu.tsx | 4 +- .../MantineSuggestionMenuItem.tsx | 0 .../SuggestionMenu/defaultGetItems.tsx | 7 +- .../hooks/useCloseSuggestionMenuNoItems.ts | 2 +- .../hooks/useSuggestionMenuData.ts | 30 ++++ .../useSuggestionMenuKeyboardNavigation.ts | 2 +- .../hooks/useSuggestionMenuPosition.ts} | 34 ++-- .../TableHandles/hooks/useTableHandlesData.ts | 55 +++++++ .../hooks/useTableHandlesPosition.ts | 139 ++++++++++++++++ .../react/src/editor/BlockNoteDefaultUI.tsx | 20 +-- packages/react/src/index.ts | 34 ++-- 49 files changed, 1346 insertions(+), 829 deletions(-) delete mode 100644 packages/core/src/extensions-shared/BaseUiElementTypes.ts create mode 100644 packages/core/src/extensions-shared/UiElementPosition.ts create mode 100644 packages/react/src/components-shared/UiComponentTypes.ts create mode 100644 packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx rename packages/react/src/components/FormattingToolbar/{FormattingToolbarPositioner.tsx => hooks/useFormattingToolbarPosition.ts} (55%) create mode 100644 packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx delete mode 100644 packages/react/src/components/HyperlinkToolbar/HyperlinkToolbarPositioner.tsx create mode 100644 packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarData.ts create mode 100644 packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarPosition.ts create mode 100644 packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx delete mode 100644 packages/react/src/components/ImageToolbar/ImageToolbarPositioner.tsx create mode 100644 packages/react/src/components/ImageToolbar/hooks/useImageToolbarData.ts create mode 100644 packages/react/src/components/ImageToolbar/hooks/useImageToolbarPosition.ts create mode 100644 packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx delete mode 100644 packages/react/src/components/SideMenu/SideMenuPositioner.tsx create mode 100644 packages/react/src/components/SideMenu/hooks/useSideMenuData.ts create mode 100644 packages/react/src/components/SideMenu/hooks/useSideMenuPosition.ts create mode 100644 packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx rename packages/react/src/components/SuggestionMenu/{ => MantineDefaults}/MantineSuggestionMenu.tsx (95%) rename packages/react/src/components/SuggestionMenu/{ => MantineDefaults}/MantineSuggestionMenuItem.tsx (100%) create mode 100644 packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuData.ts rename packages/react/src/{hooks/useSuggestionMenu.ts => components/SuggestionMenu/hooks/useSuggestionMenuPosition.ts} (69%) create mode 100644 packages/react/src/components/TableHandles/hooks/useTableHandlesData.ts create mode 100644 packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts diff --git a/examples/editor/examples/react-custom-styles/App.tsx b/examples/editor/examples/react-custom-styles/App.tsx index e7af8f6386..8f19d5d626 100644 --- a/examples/editor/examples/react-custom-styles/App.tsx +++ b/examples/editor/examples/react-custom-styles/App.tsx @@ -7,7 +7,7 @@ import { import { BlockNoteView, createReactStyleSpec, - FormattingToolbarPositioner, + DefaultPositionedFormattingToolbar, Toolbar, ToolbarButton, useActiveStyles, @@ -130,7 +130,7 @@ export function ReactStyles() { return ( - diff --git a/examples/editor/examples/vanilla-custom-styles/App.tsx b/examples/editor/examples/vanilla-custom-styles/App.tsx index 0db59d1298..d48798668c 100644 --- a/examples/editor/examples/vanilla-custom-styles/App.tsx +++ b/examples/editor/examples/vanilla-custom-styles/App.tsx @@ -7,7 +7,7 @@ import { } from "@blocknote/core"; import { BlockNoteView, - FormattingToolbarPositioner, + DefaultPositionedFormattingToolbar, Toolbar, ToolbarButton, useActiveStyles, @@ -137,7 +137,7 @@ export function Styles() { return ( - diff --git a/examples/vanilla/src/ui/addFormattingToolbar.ts b/examples/vanilla/src/ui/addFormattingToolbar.ts index bae8673512..3a0a557c0e 100644 --- a/examples/vanilla/src/ui/addFormattingToolbar.ts +++ b/examples/vanilla/src/ui/addFormattingToolbar.ts @@ -2,40 +2,33 @@ import { BlockNoteEditor } from "@blocknote/core"; import { createButton } from "./util"; export const addFormattingToolbar = (editor: BlockNoteEditor) => { - let element: HTMLElement; - let boldBtn: HTMLAnchorElement; + const element = document.createElement("div"); + element.style.background = "gray"; + element.style.display = "none"; + element.style.opacity = "0.8"; + element.style.padding = "10px"; + element.style.position = "absolute"; - editor.formattingToolbar.onUpdate((formattingToolbarState) => { - if (!element) { - element = document.createElement("div"); - element.style.background = "gray"; - element.style.position = "absolute"; - element.style.padding = "10px"; - element.style.opacity = "0.8"; - boldBtn = createButton("set bold", () => { - editor.toggleStyles({ bold: true }); - }); - element.appendChild(boldBtn); - - const linkBtn = createButton("set link", () => { - editor.createLink("https://www.google.com"); - }); + const boldBtn = createButton("set bold", () => { + editor.toggleStyles({ bold: true }); + }); + element.appendChild(boldBtn); - element.appendChild(boldBtn); - element.appendChild(linkBtn); - element.style.display = "none"; + const linkBtn = createButton("set link", () => { + editor.createLink("https://www.google.com"); + }); + element.appendChild(linkBtn); - document.getElementById("root")!.appendChild(element); - } + document.getElementById("root")!.appendChild(element); - if (formattingToolbarState.show) { + editor.formattingToolbar.onPositionUpdate((position) => { + if (position.show) { element.style.display = "block"; boldBtn.text = "bold" in editor.getActiveStyles() ? "unset bold" : "set bold"; - element.style.top = formattingToolbarState.referencePos.top + "px"; - element.style.left = - formattingToolbarState.referencePos.x - element.offsetWidth + "px"; + element.style.top = position.referencePos.top + "px"; + element.style.left = position.referencePos.x - element.offsetWidth + "px"; } else { element.style.display = "none"; } diff --git a/examples/vanilla/src/ui/addHyperlinkToolbar.ts b/examples/vanilla/src/ui/addHyperlinkToolbar.ts index e495e601ec..4e09bd892b 100644 --- a/examples/vanilla/src/ui/addHyperlinkToolbar.ts +++ b/examples/vanilla/src/ui/addHyperlinkToolbar.ts @@ -2,45 +2,42 @@ import { BlockNoteEditor } from "@blocknote/core"; import { createButton } from "./util"; export const addHyperlinkToolbar = (editor: BlockNoteEditor) => { - let element: HTMLElement; - - editor.hyperlinkToolbar.onUpdate((hyperlinkToolbarState) => { - if (!element) { - element = document.createElement("div"); - element.style.background = "gray"; - element.style.position = "absolute"; - element.style.padding = "10px"; - element.style.opacity = "0.8"; - - const url = hyperlinkToolbarState.url; - const text = hyperlinkToolbarState.text; - - const editBtn = createButton("edit", () => { - const newUrl = prompt("new url") || url; - editor.hyperlinkToolbar.editHyperlink(newUrl, text); - }); - - element.appendChild(editBtn); - - const removeBtn = createButton("remove", () => { - editor.hyperlinkToolbar.deleteHyperlink(); - }); + let text = ""; + let url = ""; + + const element = document.createElement("div"); + element.style.background = "gray"; + element.style.display = "none"; + element.style.opacity = "0.8"; + element.style.padding = "10px"; + element.style.position = "absolute"; + + const editBtn = createButton("edit", () => { + url = prompt("new url") || url; + editor.hyperlinkToolbar.editHyperlink(url, text); + }); + element.appendChild(editBtn); - element.appendChild(editBtn); - element.appendChild(removeBtn); - element.style.display = "none"; + const removeBtn = createButton("remove", () => { + editor.hyperlinkToolbar.deleteHyperlink(); + }); + element.appendChild(removeBtn); - document.getElementById("root")!.appendChild(element); - } + document.getElementById("root")!.appendChild(element); - if (hyperlinkToolbarState.show) { + editor.hyperlinkToolbar.onPositionUpdate((position) => { + if (position.show) { element.style.display = "block"; - element.style.top = hyperlinkToolbarState.referencePos.top + "px"; - element.style.left = - hyperlinkToolbarState.referencePos.x - element.offsetWidth + "px"; + element.style.top = position.referencePos.top + "px"; + element.style.left = position.referencePos.x - element.offsetWidth + "px"; } else { element.style.display = "none"; } }); + + editor.hyperlinkToolbar.onDataUpdate((data) => { + url = data.url; + text = data.text; + }); }; diff --git a/examples/vanilla/src/ui/addSideMenu.ts b/examples/vanilla/src/ui/addSideMenu.ts index de277aabcc..845b7e9667 100644 --- a/examples/vanilla/src/ui/addSideMenu.ts +++ b/examples/vanilla/src/ui/addSideMenu.ts @@ -2,39 +2,34 @@ import { BlockNoteEditor } from "@blocknote/core"; import { createButton } from "./util"; export const addSideMenu = (editor: BlockNoteEditor) => { - let element: HTMLElement; + const element = document.createElement("div"); + element.style.background = "gray"; + element.style.display = "none"; + element.style.opacity = "0.8"; + element.style.padding = "10px"; + element.style.position = "absolute"; - editor.sideMenu.onUpdate((sideMenuState) => { - if (!element) { - element = document.createElement("div"); - element.style.background = "gray"; - element.style.position = "absolute"; - element.style.padding = "10px"; - element.style.opacity = "0.8"; - const addBtn = createButton("+", () => { - editor.sideMenu.addBlock(); - }); - element.appendChild(addBtn); - - const dragBtn = createButton("::", () => { - // TODO: render a submenu with a delete option that calls "props.deleteBlock" - }); + const addBtn = createButton("+", () => { + editor.sideMenu.addBlock(); + }); + element.appendChild(addBtn); - dragBtn.addEventListener("dragstart", editor.sideMenu.blockDragStart); - dragBtn.addEventListener("dragend", editor.sideMenu.blockDragEnd); - dragBtn.draggable = true; - element.style.display = "none"; - element.appendChild(dragBtn); + const dragBtn = createButton("::", () => { + // TODO: render a submenu with a delete option that calls "props.deleteBlock" + }); + dragBtn.addEventListener("dragstart", editor.sideMenu.blockDragStart); + dragBtn.addEventListener("dragend", editor.sideMenu.blockDragEnd); + dragBtn.draggable = true; + element.appendChild(dragBtn); - document.getElementById("root")!.appendChild(element); - } + document.getElementById("root")!.appendChild(element); - if (sideMenuState.show) { + editor.sideMenu.onPositionUpdate((position) => { + if (position.show) { element.style.display = "block"; - element.style.top = sideMenuState.referencePos.top + "px"; - element.style.left = - sideMenuState.referencePos.x - element.offsetWidth + "px"; + element.style.top = position.referencePos.top + "px"; + element.style.left = position.referencePos.x - element.offsetWidth + "px"; } else { element.style.display = "none"; } diff --git a/examples/vanilla/src/ui/addSlashMenu.ts b/examples/vanilla/src/ui/addSlashMenu.ts index 98355a43d4..8142191ab0 100644 --- a/examples/vanilla/src/ui/addSlashMenu.ts +++ b/examples/vanilla/src/ui/addSlashMenu.ts @@ -3,6 +3,73 @@ import { createButton } from "./util"; export const addSlashMenu = async (editor: BlockNoteEditor) => { let element: HTMLElement; + const getItems = async (query: string) => { + const items = [ + { + text: "Heading", + aliases: ["h", "heading1", "h1"], + executeItem: () => { + editor.suggestionMenus.closeMenu(); + editor.suggestionMenus.clearQuery(); + + editor.insertBlocks( + [ + { + type: "heading", + }, + ], + editor.getTextCursorPosition().block, + "after" + ); + }, + }, + { + text: "List", + aliases: ["ul", "li", "list", "bulletlist", "bullet list"], + executeItem: () => { + editor.suggestionMenus.closeMenu(); + editor.suggestionMenus.clearQuery(); + + editor.insertBlocks( + [ + { + type: "bulletListItem", + }, + ], + editor.getTextCursorPosition().block, + "after" + ); + }, + }, + { + text: "Paragraph", + aliases: ["p", "paragraph"], + executeItem: () => { + editor.suggestionMenus.closeMenu(); + editor.suggestionMenus.clearQuery(); + + editor.insertBlocks( + [ + { + type: "paragraph", + }, + ], + editor.getTextCursorPosition().block, + "after" + ); + }, + }, + ]; + + return items.filter( + ({ text, aliases }) => + text.toLowerCase().startsWith(query.toLowerCase()) || + (aliases && + aliases.filter((alias) => + alias.toLowerCase().startsWith(query.toLowerCase()) + ).length !== 0) + ); + }; async function updateItems( query: string, @@ -21,7 +88,7 @@ export const addSlashMenu = async (editor: BlockNoteEditor) => { return domItems; } - editor.suggestionMenus.onUpdate("slashMenu", async (slashMenuState) => { + editor.suggestionMenus.onDataUpdate("/", async (slashMenuData) => { if (!element) { element = document.createElement("div"); element.style.background = "gray"; @@ -33,83 +100,16 @@ export const addSlashMenu = async (editor: BlockNoteEditor) => { document.getElementById("root")!.appendChild(element); } - if (slashMenuState.show) { - // TODO: Default vanilla getItems data type? - const getItems = async (query: string) => { - const items = [ - { - text: "Heading", - aliases: ["h", "heading1", "h1"], - executeItem: () => { - editor.suggestionMenus.closeMenu(); - editor.suggestionMenus.clearQuery(); - - editor.insertBlocks( - [ - { - type: "heading", - }, - ], - editor.getTextCursorPosition().block, - "after" - ); - }, - }, - { - text: "List", - aliases: ["ul", "li", "list", "bulletlist", "bullet list"], - executeItem: () => { - editor.suggestionMenus.closeMenu(); - editor.suggestionMenus.clearQuery(); - - editor.insertBlocks( - [ - { - type: "bulletListItem", - }, - ], - editor.getTextCursorPosition().block, - "after" - ); - }, - }, - { - text: "Paragraph", - aliases: ["p", "paragraph"], - executeItem: () => { - editor.suggestionMenus.closeMenu(); - editor.suggestionMenus.clearQuery(); - - editor.insertBlocks( - [ - { - type: "paragraph", - }, - ], - editor.getTextCursorPosition().block, - "after" - ); - }, - }, - ]; - - return items.filter( - ({ text, aliases }) => - text.toLowerCase().startsWith(query.toLowerCase()) || - (aliases && - aliases.filter((alias) => - alias.toLowerCase().startsWith(query.toLowerCase()) - ).length !== 0) - ); - }; - - await updateItems(slashMenuState.query, getItems); + await updateItems(slashMenuData.query, getItems); + }); + editor.suggestionMenus.onPositionUpdate("/", (slashMenuPosition) => { + if (slashMenuPosition.show) { element.style.display = "block"; - element.style.top = slashMenuState.referencePos.top + "px"; + element.style.top = slashMenuPosition.referencePos.top + "px"; element.style.left = - slashMenuState.referencePos.x - element.offsetWidth + "px"; + slashMenuPosition.referencePos.x - element.offsetWidth + "px"; } else { element.style.display = "none"; } diff --git a/packages/core/src/extensions-shared/BaseUiElementTypes.ts b/packages/core/src/extensions-shared/BaseUiElementTypes.ts deleted file mode 100644 index 4ca4cc9f4c..0000000000 --- a/packages/core/src/extensions-shared/BaseUiElementTypes.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type BaseUiElementCallbacks = { - destroy: () => void; -}; - -export type BaseUiElementState = { - show: boolean; - referencePos: DOMRect; -}; diff --git a/packages/core/src/extensions-shared/UiElementPosition.ts b/packages/core/src/extensions-shared/UiElementPosition.ts new file mode 100644 index 0000000000..59b0a61463 --- /dev/null +++ b/packages/core/src/extensions-shared/UiElementPosition.ts @@ -0,0 +1,4 @@ +export type UiElementPosition = { + show: boolean; + referencePos: DOMRect; +}; diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index dac58aae43..6597a60d2a 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -3,20 +3,13 @@ import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { - BaseUiElementCallbacks, - BaseUiElementState, -} from "../../extensions-shared/BaseUiElementTypes"; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; import { EventEmitter } from "../../util/EventEmitter"; -export type FormattingToolbarCallbacks = BaseUiElementCallbacks; - -export type FormattingToolbarState = BaseUiElementState; - export class FormattingToolbarView { - private formattingToolbarState?: FormattingToolbarState; - public updateFormattingToolbar: () => void; + private position?: UiElementPosition; + private readonly updatePosition: () => void; public preventHide = false; public preventShow = false; @@ -36,18 +29,16 @@ export class FormattingToolbarView { StyleSchema >, private readonly pmView: EditorView, - updateFormattingToolbar: ( - formattingToolbarState: FormattingToolbarState - ) => void + updatePosition: (position: UiElementPosition) => void ) { - this.updateFormattingToolbar = () => { - if (!this.formattingToolbarState) { + this.updatePosition = () => { + if (!this.position) { throw new Error( "Attempting to update uninitialized formatting toolbar" ); } - updateFormattingToolbar(this.formattingToolbarState); + updatePosition(this.position); }; pmView.dom.addEventListener("mousedown", this.viewMousedownHandler); @@ -72,9 +63,9 @@ export class FormattingToolbarView { // For dragging the whole editor. dragHandler = () => { - if (this.formattingToolbarState?.show) { - this.formattingToolbarState.show = false; - this.updateFormattingToolbar(); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } }; @@ -105,16 +96,16 @@ export class FormattingToolbarView { return; } - if (this.formattingToolbarState?.show) { - this.formattingToolbarState.show = false; - this.updateFormattingToolbar(); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } }; scrollHandler = () => { - if (this.formattingToolbarState?.show) { - this.formattingToolbarState.referencePos = this.getSelectionBoundingBox(); - this.updateFormattingToolbar(); + if (this.position?.show) { + this.position.referencePos = this.getSelectionBoundingBox(); + this.updatePosition(); } }; @@ -152,24 +143,24 @@ export class FormattingToolbarView { !this.preventShow && (shouldShow || this.preventHide) ) { - this.formattingToolbarState = { + this.position = { show: true, referencePos: this.getSelectionBoundingBox(), }; - this.updateFormattingToolbar(); + this.updatePosition(); return; } // Checks if menu should be hidden. if ( - this.formattingToolbarState?.show && + this.position?.show && !this.preventHide && (!shouldShow || this.preventShow || !this.editor.isEditable) ) { - this.formattingToolbarState.show = false; - this.updateFormattingToolbar(); + this.position.show = false; + this.updatePosition(); return; } @@ -221,15 +212,19 @@ export class FormattingToolbarProsemirrorPlugin extends EventEmitter { this.plugin = new Plugin({ key: formattingToolbarPluginKey, view: (editorView) => { - this.view = new FormattingToolbarView(editor, editorView, (state) => { - this.emit("update", state); - }); + this.view = new FormattingToolbarView( + editor, + editorView, + (position) => { + this.emit("update position", position); + } + ); return this.view; }, }); } - public onUpdate(callback: (state: FormattingToolbarState) => void) { - return this.on("update", callback); + public onPositionUpdate(callback: (position: UiElementPosition) => void) { + return this.on("update position", callback); } } diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts index 30aa623af3..5cc0f78a47 100644 --- a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts @@ -3,11 +3,11 @@ import { EditorView } from "@tiptap/pm/view"; import { Mark } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { BaseUiElementState } from "../../extensions-shared/BaseUiElementTypes"; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; import { EventEmitter } from "../../util/EventEmitter"; -export type HyperlinkToolbarState = BaseUiElementState & { +export type HyperlinkToolbarData = { // The hovered hyperlink's URL, and the text it's displayed with in the // editor. url: string; @@ -15,8 +15,10 @@ export type HyperlinkToolbarState = BaseUiElementState & { }; class HyperlinkToolbarView { - private hyperlinkToolbarState?: HyperlinkToolbarState; - public updateHyperlinkToolbar: () => void; + private data?: HyperlinkToolbarData; + private position?: UiElementPosition; + private readonly updateData: () => void; + private readonly updatePosition: () => void; menuUpdateTimer: ReturnType | undefined; startMenuUpdateTimer: () => void; @@ -34,16 +36,23 @@ class HyperlinkToolbarView { constructor( private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, - updateHyperlinkToolbar: ( - hyperlinkToolbarState: HyperlinkToolbarState - ) => void + updateData: (data: HyperlinkToolbarData) => void, + updatePosition: (position: UiElementPosition) => void ) { - this.updateHyperlinkToolbar = () => { - if (!this.hyperlinkToolbarState) { + this.updateData = () => { + if (!this.data) { throw new Error("Attempting to update uninitialized hyperlink toolbar"); } - updateHyperlinkToolbar(this.hyperlinkToolbarState); + updateData(this.data); + }; + + this.updatePosition = () => { + if (!this.position) { + throw new Error("Attempting to update uninitialized hyperlink toolbar"); + } + + updatePosition(this.position); }; this.startMenuUpdateTimer = () => { @@ -124,22 +133,22 @@ class HyperlinkToolbarView { editorWrapper.contains(event.target as Node) ) ) { - if (this.hyperlinkToolbarState?.show) { - this.hyperlinkToolbarState.show = false; - this.updateHyperlinkToolbar(); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } } }; scrollHandler = () => { if (this.hyperlinkMark !== undefined) { - if (this.hyperlinkToolbarState?.show) { - this.hyperlinkToolbarState.referencePos = posToDOMRect( + if (this.position?.show) { + this.position.referencePos = posToDOMRect( this.pmView, this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to ); - this.updateHyperlinkToolbar(); + this.updatePosition(); } } }; @@ -158,9 +167,9 @@ class HyperlinkToolbarView { this.pmView.dispatch(tr); this.pmView.focus(); - if (this.hyperlinkToolbarState?.show) { - this.hyperlinkToolbarState.show = false; - this.updateHyperlinkToolbar(); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } } @@ -176,9 +185,9 @@ class HyperlinkToolbarView { ); this.pmView.focus(); - if (this.hyperlinkToolbarState?.show) { - this.hyperlinkToolbarState.show = false; - this.updateHyperlinkToolbar(); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } } @@ -232,32 +241,35 @@ class HyperlinkToolbarView { } if (this.hyperlinkMark && this.editor.isEditable) { - this.hyperlinkToolbarState = { - show: true, - referencePos: posToDOMRect( - this.pmView, + this.data = { + url: this.hyperlinkMark!.attrs.href, + text: this.pmView.state.doc.textBetween( this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to ), - url: this.hyperlinkMark!.attrs.href, - text: this.pmView.state.doc.textBetween( + }; + this.position = { + show: true, + referencePos: posToDOMRect( + this.pmView, this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to ), }; - this.updateHyperlinkToolbar(); + this.updateData(); + this.updatePosition(); return; } // Hides menu. if ( - this.hyperlinkToolbarState?.show && + this.position?.show && prevHyperlinkMark && (!this.hyperlinkMark || !this.editor.isEditable) ) { - this.hyperlinkToolbarState.show = false; - this.updateHyperlinkToolbar(); + this.position.show = false; + this.updatePosition(); return; } @@ -287,16 +299,27 @@ export class HyperlinkToolbarProsemirrorPlugin< this.plugin = new Plugin({ key: hyperlinkToolbarPluginKey, view: (editorView) => { - this.view = new HyperlinkToolbarView(editor, editorView, (state) => { - this.emit("update", state); - }); + this.view = new HyperlinkToolbarView( + editor, + editorView, + (data) => { + this.emit("update data", data); + }, + (position) => { + this.emit("update position", position); + } + ); return this.view; }, }); } - public onUpdate(callback: (state: HyperlinkToolbarState) => void) { - return this.on("update", callback); + public onDataUpdate(callback: (state: HyperlinkToolbarData) => void) { + return this.on("update data", callback); + } + + public onPositionUpdate(callback: (state: UiElementPosition) => void) { + return this.on("update position", callback); } /** diff --git a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts index 98fe041755..72d129434a 100644 --- a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +++ b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts @@ -9,17 +9,13 @@ import { SpecificBlock, StyleSchema, } from "../../schema"; -import { - BaseUiElementCallbacks, - BaseUiElementState, -} from "../../extensions-shared/BaseUiElementTypes"; -export type ImageToolbarCallbacks = BaseUiElementCallbacks; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; -export type ImageToolbarState< +export type ImageToolbarData< B extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema = StyleSchema -> = BaseUiElementState & { +> = { block: SpecificBlock; }; @@ -28,24 +24,33 @@ export class ImageToolbarView< I extends InlineContentSchema, S extends StyleSchema > { - private imageToolbarState?: ImageToolbarState; - public updateImageToolbar: () => void; + private data?: ImageToolbarData; + private position?: UiElementPosition; + private readonly updateData: () => void; + private readonly updatePosition: () => void; public prevWasEditable: boolean | null = null; constructor( private readonly pluginKey: PluginKey, private readonly pmView: EditorView, - updateImageToolbar: ( - imageToolbarState: ImageToolbarState - ) => void + updateData: (data: ImageToolbarData) => void, + updatePosition: (position: UiElementPosition) => void ) { - this.updateImageToolbar = () => { - if (!this.imageToolbarState) { + this.updateData = () => { + if (!this.data) { + throw new Error("Attempting to update uninitialized image toolbar"); + } + + updateData(this.data); + }; + + this.updatePosition = () => { + if (!this.position) { throw new Error("Attempting to update uninitialized image toolbar"); } - updateImageToolbar(this.imageToolbarState); + updatePosition(this.position); }; pmView.dom.addEventListener("mousedown", this.mouseDownHandler); @@ -58,17 +63,17 @@ export class ImageToolbarView< } mouseDownHandler = () => { - if (this.imageToolbarState?.show) { - this.imageToolbarState.show = false; - this.updateImageToolbar(); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } }; // For dragging the whole editor. dragstartHandler = () => { - if (this.imageToolbarState?.show) { - this.imageToolbarState.show = false; - this.updateImageToolbar(); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } }; @@ -88,21 +93,20 @@ export class ImageToolbarView< return; } - if (this.imageToolbarState?.show) { - this.imageToolbarState.show = false; - this.updateImageToolbar(); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } }; scrollHandler = () => { - if (this.imageToolbarState?.show) { + if (this.position?.show) { const blockElement = document.querySelector( - `[data-node-type="blockContainer"][data-id="${this.imageToolbarState.block.id}"]` + `[data-node-type="blockContainer"][data-id="${this.data!.block.id}"]` )!; - this.imageToolbarState.referencePos = - blockElement.getBoundingClientRect(); - this.updateImageToolbar(); + this.position.referencePos = blockElement.getBoundingClientRect(); + this.updatePosition(); } }; @@ -111,18 +115,21 @@ export class ImageToolbarView< block: SpecificBlock; } = this.pluginKey.getState(view.state); - if (!this.imageToolbarState?.show && pluginState.block) { + if (!this.position?.show && pluginState.block) { const blockElement = document.querySelector( `[data-node-type="blockContainer"][data-id="${pluginState.block.id}"]` )!; - this.imageToolbarState = { + this.data = { + block: pluginState.block, + }; + this.position = { show: true, referencePos: blockElement.getBoundingClientRect(), - block: pluginState.block, }; - this.updateImageToolbar(); + this.updateData(); + this.updatePosition(); return; } @@ -131,10 +138,10 @@ export class ImageToolbarView< !view.state.selection.eq(prevState.selection) || !view.state.doc.eq(prevState.doc) ) { - if (this.imageToolbarState?.show) { - this.imageToolbarState.show = false; + if (this.position?.show) { + this.position.show = false; - this.updateImageToolbar(); + this.updatePosition(); } } } @@ -171,8 +178,11 @@ export class ImageToolbarProsemirrorPlugin< // editor, imageToolbarPluginKey, editorView, - (state) => { - this.emit("update", state); + (data) => { + this.emit("update data", data); + }, + (position) => { + this.emit("update position", position); } ); return this.view; @@ -195,7 +205,13 @@ export class ImageToolbarProsemirrorPlugin< }); } - public onUpdate(callback: (state: ImageToolbarState) => void) { - return this.on("update", callback); + public onDataUpdate( + callback: (data: ImageToolbarData) => void + ) { + return this.on("update data", callback); + } + + public onPositionUpdate(callback: (position: UiElementPosition) => void) { + return this.on("update position", callback); } } diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index a468dc989a..e2797efac3 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -7,7 +7,7 @@ import { createInternalHTMLSerializer } from "../../api/exporters/html/internalH import { cleanHTMLToMarkdown } from "../../api/exporters/markdown/markdownExporter"; import { getBlockInfoFromPos } from "../../api/getBlockInfoFromPos"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { BaseUiElementState } from "../../extensions-shared/BaseUiElementTypes"; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { Block, BlockSchema, @@ -20,11 +20,11 @@ import { suggestionMenuPluginKey } from "../SuggestionMenu/SuggestionPlugin"; let dragImageElement: Element | undefined; -export type SideMenuState< +export type SideMenuData< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema -> = BaseUiElementState & { +> = { // The block that the side menu is attached to. block: Block; }; @@ -255,7 +255,10 @@ export class SideMenuView< S extends StyleSchema > implements PluginView { - private sideMenuState?: SideMenuState; + private data?: SideMenuData; + private position?: UiElementPosition; + private readonly updateData: () => void; + private readonly updatePosition: () => void; // 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 @@ -273,10 +276,25 @@ export class SideMenuView< constructor( private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, - private readonly updateSideMenu: ( - sideMenuState: SideMenuState - ) => void + updateData: (data: SideMenuData) => void, + updatePosition: (position: UiElementPosition) => void ) { + this.updateData = () => { + if (!this.data) { + throw new Error("Attempting to update uninitialized side menu"); + } + + updateData(this.data); + }; + + this.updatePosition = () => { + if (!this.position) { + throw new Error("Attempting to update uninitialized side menu"); + } + + updatePosition(this.position); + }; + this.horizontalPosAnchoredAtRoot = true; this.horizontalPosAnchor = ( this.pmView.dom.firstChild! as HTMLElement @@ -369,17 +387,17 @@ export class SideMenuView< }; onKeyDown = (_event: KeyboardEvent) => { - if (this.sideMenuState?.show) { - this.sideMenuState.show = false; - this.updateSideMenu(this.sideMenuState); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } this.menuFrozen = false; }; onMouseDown = (_event: MouseEvent) => { - if (this.sideMenuState && !this.sideMenuState.show) { - this.sideMenuState.show = true; - this.updateSideMenu(this.sideMenuState); + if (this.position && !this.position.show) { + this.position.show = true; + this.updatePosition(); } this.menuFrozen = false; }; @@ -421,9 +439,9 @@ export class SideMenuView< editorWrapper.contains(event.target as HTMLElement) ) ) { - if (this.sideMenuState?.show) { - this.sideMenuState.show = false; - this.updateSideMenu(this.sideMenuState); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } return; @@ -440,9 +458,9 @@ export class SideMenuView< // Closes the menu if the mouse cursor is beyond the editor vertically. if (!block || !this.editor.isEditable) { - if (this.sideMenuState?.show) { - this.sideMenuState.show = false; - this.updateSideMenu(this.sideMenuState); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } return; @@ -450,7 +468,7 @@ export class SideMenuView< // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block. if ( - this.sideMenuState?.show && + this.position?.show && this.hoveredBlock?.hasAttribute("data-id") && this.hoveredBlock?.getAttribute("data-id") === block.id ) { @@ -470,7 +488,12 @@ export class SideMenuView< if (this.editor.isEditable) { const blockContentBoundingBox = blockContent.getBoundingClientRect(); - this.sideMenuState = { + this.data = { + block: this.editor.getBlock( + this.hoveredBlock!.getAttribute("data-id")! + )!, + }; + this.position = { show: true, referencePos: new DOMRect( this.horizontalPosAnchoredAtRoot @@ -480,21 +503,19 @@ export class SideMenuView< blockContentBoundingBox.width, blockContentBoundingBox.height ), - block: this.editor.getBlock( - this.hoveredBlock!.getAttribute("data-id")! - )!, }; - this.updateSideMenu(this.sideMenuState); + this.updateData(); + this.updatePosition(); } }; onScroll = () => { - if (this.sideMenuState?.show) { + if (this.position?.show) { const blockContent = this.hoveredBlock!.firstChild as HTMLElement; const blockContentBoundingBox = blockContent.getBoundingClientRect(); - this.sideMenuState.referencePos = new DOMRect( + this.position.referencePos = new DOMRect( this.horizontalPosAnchoredAtRoot ? this.horizontalPosAnchor : blockContentBoundingBox.x, @@ -502,14 +523,14 @@ export class SideMenuView< blockContentBoundingBox.width, blockContentBoundingBox.height ); - this.updateSideMenu(this.sideMenuState); + this.updatePosition(); } }; destroy() { - if (this.sideMenuState?.show) { - this.sideMenuState.show = false; - this.updateSideMenu(this.sideMenuState); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } document.body.removeEventListener("mousemove", this.onMouseMove); document.body.removeEventListener("dragover", this.onDragOver); @@ -521,9 +542,9 @@ export class SideMenuView< } addBlock() { - if (this.sideMenuState?.show) { - this.sideMenuState.show = false; - this.updateSideMenu(this.sideMenuState); + if (this.position?.show) { + this.position.show = false; + this.updatePosition(); } this.menuFrozen = true; @@ -596,8 +617,11 @@ export class SideMenuProsemirrorPlugin< this.sideMenuView = new SideMenuView( editor, editorView, - (sideMenuState) => { - this.emit("update", sideMenuState); + (data) => { + this.emit("update data", data); + }, + (position) => { + this.emit("update position", position); } ); return this.sideMenuView; @@ -605,8 +629,12 @@ export class SideMenuProsemirrorPlugin< }); } - public onUpdate(callback: (state: SideMenuState) => void) { - return this.on("update", callback); + public onDataUpdate(callback: (data: SideMenuData) => void) { + return this.on("update data", callback); + } + + public onPositionUpdate(callback: (position: UiElementPosition) => void) { + return this.on("update position", callback); } /** diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index 08df513129..88c84a83b1 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -4,11 +4,11 @@ import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; import { EventEmitter } from "../../util/EventEmitter"; -import { BaseUiElementState } from "../../extensions-shared/BaseUiElementTypes"; -// TODO: clean file +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; + const findBlock = findParentNode((node) => node.type.name === "blockContainer"); -export type SuggestionsMenuState = BaseUiElementState & { +export type SuggestionMenuData = { query: string; }; @@ -17,39 +17,49 @@ class SuggestionMenuView< I extends InlineContentSchema, S extends StyleSchema > { - private suggestionsMenuState?: SuggestionsMenuState; - public updateSuggestionsMenu: (triggerCharacter: string) => void; + private data?: SuggestionMenuData; + private position?: UiElementPosition; + public updateData: (triggerCharacter: string) => void; + public updatePosition: (triggerCharacter: string) => void; pluginState: SuggestionPluginState; constructor( private readonly editor: BlockNoteEditor, - updateSuggestionsMenu: ( - menuName: string, - suggestionsMenuState: SuggestionsMenuState + updateData: (triggerCharacter: string, data: SuggestionMenuData) => void, + updatePosition: ( + triggerCharacter: string, + position: UiElementPosition ) => void ) { this.pluginState = undefined; - this.updateSuggestionsMenu = (menuName: string) => { - if (!this.suggestionsMenuState) { + this.updateData = (triggerCharacter: string) => { + if (!this.data) { + throw new Error("Attempting to update uninitialized suggestions menu"); + } + + updateData(triggerCharacter, this.data); + }; + + this.updatePosition = (triggerCharacter: string) => { + if (!this.position) { throw new Error("Attempting to update uninitialized suggestions menu"); } - updateSuggestionsMenu(menuName, this.suggestionsMenuState); + updatePosition(triggerCharacter, this.position); }; document.addEventListener("scroll", this.handleScroll); } handleScroll = () => { - if (this.suggestionsMenuState?.show) { + if (this.position?.show) { const decorationNode = document.querySelector( `[data-decoration-id="${this.pluginState!.decorationId}"]` ); - this.suggestionsMenuState.referencePos = - decorationNode!.getBoundingClientRect(); - this.updateSuggestionsMenu(this.pluginState!.triggerCharacter!); + this.position.referencePos = decorationNode!.getBoundingClientRect(); + this.updatePosition(this.pluginState!.triggerCharacter!); } }; @@ -73,8 +83,8 @@ class SuggestionMenuView< this.pluginState = stopped ? prev : next; if (stopped || !this.editor.isEditable) { - this.suggestionsMenuState!.show = false; - this.updateSuggestionsMenu(this.pluginState!.triggerCharacter); + this.position!.show = false; + this.updatePosition(this.pluginState!.triggerCharacter); return; } @@ -84,13 +94,20 @@ class SuggestionMenuView< ); if (this.editor.isEditable) { - this.suggestionsMenuState = { - show: true, - referencePos: decorationNode!.getBoundingClientRect(), + this.data = { query: this.pluginState!.query, }; - this.updateSuggestionsMenu(this.pluginState!.triggerCharacter!); + this.updateData(this.pluginState!.triggerCharacter!); + + if (started) { + this.position = { + show: true, + referencePos: decorationNode!.getBoundingClientRect(), + }; + + this.updatePosition(this.pluginState!.triggerCharacter); + } } } @@ -168,8 +185,14 @@ export class SuggestionMenuProseMirrorPlugin< view: () => { this.view = new SuggestionMenuView( editor, - (triggerCharacter, suggestionsMenuState) => { - this.emit(`update ${triggerCharacter}`, suggestionsMenuState); + (triggerCharacter, suggestionMenuData) => { + this.emit(`update data ${triggerCharacter}`, suggestionMenuData); + }, + (triggerCharacter, suggestionMenuPosition) => { + this.emit( + `update position ${triggerCharacter}`, + suggestionMenuPosition + ); } ); return this.view; @@ -320,21 +343,38 @@ export class SuggestionMenuProseMirrorPlugin< }); } - public onUpdate( + public onDataUpdate( triggerCharacter: string, - callback: (state: SuggestionsMenuState) => void + callback: (data: SuggestionMenuData) => void ) { if (!this.triggerCharacters.includes(triggerCharacter)) { this.addTriggerCharacter(triggerCharacter); } // TODO: be able to remove the triggerCharacter - return this.on(`update ${triggerCharacter}`, callback); + return this.on(`update data ${triggerCharacter}`, callback); + } + + public onPositionUpdate( + triggerCharacter: string, + callback: (position: UiElementPosition) => void + ) { + if (!this.triggerCharacters.includes(triggerCharacter)) { + this.addTriggerCharacter(triggerCharacter); + } + return this.on(`update position ${triggerCharacter}`, callback); } addTriggerCharacter = (triggerCharacter: string) => { this.triggerCharacters.push(triggerCharacter); }; + // TODO: Should this be called automatically when listeners are removed? + removeTriggerCharacter = (triggerCharacter: string) => { + this.triggerCharacters = this.triggerCharacters.filter( + (c) => c !== triggerCharacter + ); + }; + closeMenu = () => this.view!.closeMenu(); clearQuery = () => this.view!.clearQuery(); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 62046f1836..3a795c5157 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,7 +13,7 @@ export * from "./extensions/ImageToolbar/ImageToolbarPlugin"; export * from "./extensions/SuggestionMenu/SuggestionPlugin"; export * from "./extensions/SideMenu/SideMenuPlugin"; export * from "./extensions/TableHandles/TableHandlesPlugin"; -export * from "./extensions-shared/BaseUiElementTypes"; +export * from "./extensions-shared/UiElementPosition"; export * from "./schema"; export * from "./util/browser"; export * from "./util/string"; diff --git a/packages/react/src/components-shared/UiComponentTypes.ts b/packages/react/src/components-shared/UiComponentTypes.ts new file mode 100644 index 0000000000..cc2b299455 --- /dev/null +++ b/packages/react/src/components-shared/UiComponentTypes.ts @@ -0,0 +1,17 @@ +import { BlockNoteEditor } from "@blocknote/core"; +import { CSSProperties } from "react"; + +export type UiComponentData< + UiElementData, + UiElementPluginName extends keyof BlockNoteEditor +> = UiElementData & + Omit< + BlockNoteEditor[UiElementPluginName], + "plugin" | "on" | "onPositionUpdate" | "onDataUpdate" | "off" + >; + +export type UiComponentPosition = { + isMounted: boolean; + ref: (node: HTMLElement | null) => void; + style: CSSProperties; +}; diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/ReplaceImageButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/ReplaceImageButton.tsx index b58abd4423..9e27b7c507 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/ReplaceImageButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/ReplaceImageButton.tsx @@ -39,10 +39,7 @@ export const ReplaceImageButton = (props: { /> - + ); diff --git a/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx b/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx index ca0cffbc06..39468400e2 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx @@ -1,27 +1,25 @@ -import { BlockSchema } from "@blocknote/core"; +import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; import { Toolbar } from "../../components-shared/Toolbar/Toolbar"; -import { ColorStyleButton } from "./DefaultButtons/ColorStyleButton"; -import { CreateLinkButton } from "./DefaultButtons/CreateLinkButton"; +import { + BlockTypeDropdown, + BlockTypeDropdownItem, +} from "./DefaultDropdowns/BlockTypeDropdown"; import { ImageCaptionButton } from "./DefaultButtons/ImageCaptionButton"; +import { ReplaceImageButton } from "./DefaultButtons/ReplaceImageButton"; +import { ToggledStyleButton } from "./DefaultButtons/ToggledStyleButton"; +import { TextAlignButton } from "./DefaultButtons/TextAlignButton"; +import { ColorStyleButton } from "./DefaultButtons/ColorStyleButton"; import { NestBlockButton, UnnestBlockButton, } from "./DefaultButtons/NestBlockButtons"; -import { ReplaceImageButton } from "./DefaultButtons/ReplaceImageButton"; -import { TextAlignButton } from "./DefaultButtons/TextAlignButton"; -import { ToggledStyleButton } from "./DefaultButtons/ToggledStyleButton"; -import { - BlockTypeDropdown, - BlockTypeDropdownItem, -} from "./DefaultDropdowns/BlockTypeDropdown"; -import type { FormattingToolbarProps } from "./FormattingToolbarPositioner"; +import { CreateLinkButton } from "./DefaultButtons/CreateLinkButton"; -export const DefaultFormattingToolbar = ( - props: FormattingToolbarProps & { - blockTypeDropdownItems?: BlockTypeDropdownItem[]; - } -) => { +export const DefaultFormattingToolbar = (props: { + editor: BlockNoteEditor; + blockTypeDropdownItems?: BlockTypeDropdownItem[]; +}) => { return ( diff --git a/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx b/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx new file mode 100644 index 0000000000..48777c509c --- /dev/null +++ b/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx @@ -0,0 +1,30 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, +} from "@blocknote/core"; +import { FC } from "react"; + +import { useFormattingToolbarPosition } from "./hooks/useFormattingToolbarPosition"; +import { DefaultFormattingToolbar } from "./DefaultFormattingToolbar"; + +export const DefaultPositionedFormattingToolbar = < + BSchema extends BlockSchema = DefaultBlockSchema +>(props: { + editor: BlockNoteEditor; + formattingToolbar?: FC<{ editor: BlockNoteEditor }>; +}) => { + const { isMounted, ref, style } = useFormattingToolbarPosition(props.editor); + + if (!isMounted) { + return null; + } + + const FormattingToolbar = props.formattingToolbar || DefaultFormattingToolbar; + + return ( +
+ +
+ ); +}; diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbarPositioner.tsx b/packages/react/src/components/FormattingToolbar/hooks/useFormattingToolbarPosition.ts similarity index 55% rename from packages/react/src/components/FormattingToolbar/FormattingToolbarPositioner.tsx rename to packages/react/src/components/FormattingToolbar/hooks/useFormattingToolbarPosition.ts index 24f52f1937..b6c9fbd17f 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbarPositioner.tsx +++ b/packages/react/src/components/FormattingToolbar/hooks/useFormattingToolbarPosition.ts @@ -2,7 +2,11 @@ import { BlockNoteEditor, BlockSchema, DefaultBlockSchema, + DefaultInlineContentSchema, DefaultProps, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, } from "@blocknote/core"; import { flip, @@ -10,10 +14,9 @@ import { useFloating, useTransitionStyles, } from "@floating-ui/react"; -import { FC, useEffect, useRef, useState } from "react"; - -import { useEditorChange } from "../../hooks/useEditorChange"; -import { DefaultFormattingToolbar } from "./DefaultFormattingToolbar"; +import { useEffect, useRef, useState } from "react"; +import { useEditorChange } from "../../../hooks/useEditorChange"; +import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; const textAlignmentToPlacement = ( textAlignment: DefaultProps["textAlignment"] @@ -30,22 +33,16 @@ const textAlignmentToPlacement = ( } }; -export type FormattingToolbarProps< - BSchema extends BlockSchema = DefaultBlockSchema -> = { - editor: BlockNoteEditor; -}; - -export const FormattingToolbarPositioner = < - BSchema extends BlockSchema = DefaultBlockSchema ->(props: { - editor: BlockNoteEditor; - formattingToolbar?: FC>; -}) => { +export function useFormattingToolbarPosition< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(editor: BlockNoteEditor): UiComponentPosition { const [show, setShow] = useState(false); + const referencePos = useRef(); const [placement, setPlacement] = useState<"top-start" | "top" | "top-end">( () => { - const block = props.editor.getTextCursorPosition().block; + const block = editor.getTextCursorPosition().block; if (!("textAlignment" in block.props)) { return "top-start"; @@ -57,8 +54,6 @@ export const FormattingToolbarPositioner = < } ); - const referencePos = useRef(); - const { refs, update, context, floatingStyles } = useFloating({ open: show, placement, @@ -68,17 +63,17 @@ export const FormattingToolbarPositioner = < const { isMounted, styles } = useTransitionStyles(context); useEffect(() => { - return props.editor.formattingToolbar.onUpdate((state) => { - setShow(state.show); + return editor.formattingToolbar.onPositionUpdate((position) => { + setShow(position.show); - referencePos.current = state.referencePos; + referencePos.current = position.referencePos; update(); }); - }, [props.editor, update]); + }, [editor, update]); - useEditorChange(props.editor, () => { - const block = props.editor.getTextCursorPosition().block; + useEditorChange(editor, () => { + const block = editor.getTextCursorPosition().block; if (!("textAlignment" in block.props)) { setPlacement("top-start"); @@ -97,17 +92,14 @@ export const FormattingToolbarPositioner = < }); }, [refs]); - const FormattingToolbar = props.formattingToolbar || DefaultFormattingToolbar; - - if (!isMounted) { - return null; - } - - return ( -
- -
- ); -}; + return { + isMounted: isMounted, + ref: refs.setFloating, + style: { + display: "flex", + ...styles, + ...floatingStyles, + zIndex: 3000, + }, + }; +} diff --git a/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx b/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx index 4e9e72f1ce..a275147e74 100644 --- a/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx +++ b/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx @@ -1,29 +1,42 @@ -import { BlockSchema, InlineContentSchema } from "@blocknote/core"; +import { + BlockNoteEditor, + BlockSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; import { useRef, useState } from "react"; import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; -import { StyleSchema } from "@blocknote/core"; +import { useHyperlinkToolbarData } from "./hooks/useHyperlinkToolbarData"; import { Toolbar } from "../../components-shared/Toolbar/Toolbar"; import { ToolbarButton } from "../../components-shared/Toolbar/ToolbarButton"; import { EditHyperlinkMenu } from "./EditHyperlinkMenu/components/EditHyperlinkMenu"; -import type { HyperlinkToolbarProps } from "./HyperlinkToolbarPositioner"; export const DefaultHyperlinkToolbar = < BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->( - props: HyperlinkToolbarProps -) => { +>(props: { + editor: BlockNoteEditor; +}) => { const [isEditing, setIsEditing] = useState(false); const editMenuRef = useRef(null); + const { + text, + url, + deleteHyperlink, + editHyperlink, + startHideTimer, + stopHideTimer, + } = useHyperlinkToolbarData(props.editor); + if (isEditing) { return ( setTimeout(() => { @@ -39,9 +52,7 @@ export const DefaultHyperlinkToolbar = < } return ( - + { - window.open(props.url, "_blank"); + window.open(url, "_blank"); }} icon={RiExternalLinkFill} /> diff --git a/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx b/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx new file mode 100644 index 0000000000..5e0f9e77d8 --- /dev/null +++ b/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx @@ -0,0 +1,36 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { FC } from "react"; + +import { useHyperlinkToolbarPosition } from "./hooks/useHyperlinkToolbarPosition"; +import { DefaultHyperlinkToolbar } from "./DefaultHyperlinkToolbar"; + +export const DefaultPositionedHyperlinkToolbar = < + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(props: { + editor: BlockNoteEditor; + hyperlinkToolbar?: FC<{ editor: BlockNoteEditor }>; +}) => { + const { isMounted, ref, style } = useHyperlinkToolbarPosition(props.editor); + + if (!isMounted) { + return null; + } + + const HyperlinkToolbar = props.hyperlinkToolbar || DefaultHyperlinkToolbar; + + return ( +
+ +
+ ); +}; diff --git a/packages/react/src/components/HyperlinkToolbar/HyperlinkToolbarPositioner.tsx b/packages/react/src/components/HyperlinkToolbar/HyperlinkToolbarPositioner.tsx deleted file mode 100644 index fcd702f7c2..0000000000 --- a/packages/react/src/components/HyperlinkToolbar/HyperlinkToolbarPositioner.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { - BaseUiElementState, - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - HyperlinkToolbarProsemirrorPlugin, - HyperlinkToolbarState, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { - flip, - offset, - useFloating, - useTransitionStyles, -} from "@floating-ui/react"; -import { FC, useEffect, useRef, useState } from "react"; - -import { DefaultHyperlinkToolbar } from "./DefaultHyperlinkToolbar"; - -export type HyperlinkToolbarProps< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema -> = Pick< - HyperlinkToolbarProsemirrorPlugin, - "editHyperlink" | "deleteHyperlink" | "startHideTimer" | "stopHideTimer" -> & - Omit; - -export const HyperlinkToolbarPositioner = < - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->(props: { - editor: BlockNoteEditor; - hyperlinkToolbar?: FC>; -}) => { - const [show, setShow] = useState(false); - const [url, setUrl] = useState(); - const [text, setText] = useState(); - - const referencePos = useRef(); - - const { refs, update, context, floatingStyles } = useFloating({ - open: show, - placement: "top-start", - middleware: [offset(10), flip()], - }); - - const { isMounted, styles } = useTransitionStyles(context); - - useEffect(() => { - return props.editor.hyperlinkToolbar.on( - "update", - (hyperlinkToolbarState) => { - setShow(hyperlinkToolbarState.show); - setUrl(hyperlinkToolbarState.url); - setText(hyperlinkToolbarState.text); - - referencePos.current = hyperlinkToolbarState.referencePos; - - update(); - } - ); - }, [props.editor, update]); - - useEffect(() => { - refs.setReference({ - getBoundingClientRect: () => referencePos.current!, - }); - }, [refs]); - - if (!url || !text || !isMounted) { - return null; - } - - const HyperlinkToolbar = props.hyperlinkToolbar || DefaultHyperlinkToolbar; - - return ( -
- -
- ); -}; diff --git a/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarData.ts b/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarData.ts new file mode 100644 index 0000000000..be8fb7a045 --- /dev/null +++ b/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarData.ts @@ -0,0 +1,39 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + HyperlinkToolbarData, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { useEffect, useState } from "react"; +import { UiComponentData } from "../../../components-shared/UiComponentTypes"; + +export function useHyperlinkToolbarData< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>( + editor: BlockNoteEditor +): UiComponentData { + const [text, setText] = useState(); + const [url, setUrl] = useState(); + + useEffect(() => { + return editor.hyperlinkToolbar.onDataUpdate((data) => { + setText(data.text); + setUrl(data.url); + }); + }, [editor]); + + return { + text: text!, + url: url!, + deleteHyperlink: editor.hyperlinkToolbar.deleteHyperlink, + editHyperlink: editor.hyperlinkToolbar.editHyperlink, + startHideTimer: editor.hyperlinkToolbar.startHideTimer, + stopHideTimer: editor.hyperlinkToolbar.stopHideTimer, + }; +} diff --git a/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarPosition.ts b/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarPosition.ts new file mode 100644 index 0000000000..c59554fabb --- /dev/null +++ b/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarPosition.ts @@ -0,0 +1,59 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { + flip, + offset, + useFloating, + useTransitionStyles, +} from "@floating-ui/react"; +import { useEffect, useRef, useState } from "react"; +import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; + +export function useHyperlinkToolbarPosition< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(editor: BlockNoteEditor): UiComponentPosition { + const [show, setShow] = useState(false); + const referencePos = useRef(); + + const { refs, update, context, floatingStyles } = useFloating({ + open: show, + placement: "top-start", + middleware: [offset(10), flip()], + }); + + const { isMounted, styles } = useTransitionStyles(context); + + useEffect(() => { + return editor.hyperlinkToolbar.onPositionUpdate((position) => { + setShow(position.show); + referencePos.current = position.referencePos; + update(); + }); + }, [editor.hyperlinkToolbar, update]); + + useEffect(() => { + refs.setReference({ + getBoundingClientRect: () => referencePos.current!, + }); + }, [refs]); + + return { + isMounted: isMounted, + ref: refs.setFloating, + style: { + display: "flex", + ...styles, + ...floatingStyles, + zIndex: 4000, + }, + }; +} diff --git a/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx b/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx index d02e1a2579..32a08ec27d 100644 --- a/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx +++ b/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx @@ -1,5 +1,4 @@ -import { BlockSchema, PartialBlock } from "@blocknote/core"; - +import { BlockNoteEditor, BlockSchema, PartialBlock } from "@blocknote/core"; import { Button, FileInput, @@ -15,18 +14,21 @@ import { useEffect, useState, } from "react"; + +import { useImageToolbarData } from "./hooks/useImageToolbarData"; import { Toolbar } from "../../components-shared/Toolbar/Toolbar"; -import type { ImageToolbarProps } from "./ImageToolbarPositioner"; -export const DefaultImageToolbar = ( - props: ImageToolbarProps -) => { +export const DefaultImageToolbar = (props: { + editor: BlockNoteEditor; +}) => { const [openTab, setOpenTab] = useState<"upload" | "embed">( props.editor.uploadFile !== undefined ? "upload" : "embed" ); const [uploading, setUploading] = useState(false); const [uploadFailed, setUploadFailed] = useState(false); + const { block } = useImageToolbarData(props.editor); + useEffect(() => { if (uploadFailed) { setTimeout(() => { @@ -47,7 +49,7 @@ export const DefaultImageToolbar = ( if (props.editor.uploadFile !== undefined) { try { const uploaded = await props.editor.uploadFile(file); - props.editor.updateBlock(props.block, { + props.editor.updateBlock(block, { type: "image", props: { url: uploaded, @@ -63,7 +65,7 @@ export const DefaultImageToolbar = ( upload(file); }, - [props.block, props.editor] + [block, props.editor] ); const [currentURL, setCurrentURL] = useState(""); @@ -79,7 +81,7 @@ export const DefaultImageToolbar = ( (event: KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault(); - props.editor.updateBlock(props.block, { + props.editor.updateBlock(block, { type: "image", props: { url: currentURL, @@ -87,17 +89,17 @@ export const DefaultImageToolbar = ( } as PartialBlock); } }, - [currentURL, props.block, props.editor] + [currentURL, block, props.editor] ); const handleURLClick = useCallback(() => { - props.editor.updateBlock(props.block, { + props.editor.updateBlock(block, { type: "image", props: { url: currentURL, }, } as PartialBlock); - }, [currentURL, props.block, props.editor]); + }, [currentURL, block, props.editor]); return ( diff --git a/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx b/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx new file mode 100644 index 0000000000..9224494920 --- /dev/null +++ b/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx @@ -0,0 +1,33 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + InlineContentSchema, +} from "@blocknote/core"; +import { FC } from "react"; + +import { DefaultImageToolbar } from "./DefaultImageToolbar"; +import { useImageToolbarPosition } from "./hooks/useImageToolbarPosition"; + +export const DefaultPositionedImageToolbar = < + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema +>(props: { + editor: BlockNoteEditor; + imageToolbar?: FC<{ editor: BlockNoteEditor }>; +}) => { + const { isMounted, ref, style } = useImageToolbarPosition(props.editor); + + if (!isMounted) { + return null; + } + + const ImageToolbar = props.imageToolbar || DefaultImageToolbar; + + return ( +
+ +
+ ); +}; diff --git a/packages/react/src/components/ImageToolbar/ImageToolbarPositioner.tsx b/packages/react/src/components/ImageToolbar/ImageToolbarPositioner.tsx deleted file mode 100644 index 1a72153522..0000000000 --- a/packages/react/src/components/ImageToolbar/ImageToolbarPositioner.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { - BaseUiElementState, - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - ImageToolbarState, - InlineContentSchema, - SpecificBlock, -} from "@blocknote/core"; -import { - flip, - offset, - useFloating, - useTransitionStyles, -} from "@floating-ui/react"; -import { FC, useEffect, useRef, useState } from "react"; - -import { DefaultImageToolbar } from "./DefaultImageToolbar"; - -export type ImageToolbarProps< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema -> = Omit, keyof BaseUiElementState> & { - editor: BlockNoteEditor; -}; - -export const ImageToolbarPositioner = < - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema ->(props: { - editor: BlockNoteEditor; - imageToolbar?: FC>; -}) => { - const [show, setShow] = useState(false); - const [block, setBlock] = useState>(); - - const referencePos = useRef(); - - const { refs, update, context, floatingStyles } = useFloating({ - open: show, - placement: "bottom", - middleware: [offset(10), flip()], - }); - - const { isMounted, styles } = useTransitionStyles(context); - - useEffect(() => { - return props.editor.imageToolbar.onUpdate((imageToolbarState) => { - setShow(imageToolbarState.show); - setBlock(imageToolbarState.block); - - referencePos.current = imageToolbarState.referencePos; - - update(); - }); - }, [props.editor, update]); - - useEffect(() => { - refs.setReference({ - getBoundingClientRect: () => referencePos.current!, - }); - }, [refs]); - - const ImageToolbar = props.imageToolbar || DefaultImageToolbar; - - if (!isMounted) { - return null; - } - - return ( -
- -
- ); -}; diff --git a/packages/react/src/components/ImageToolbar/hooks/useImageToolbarData.ts b/packages/react/src/components/ImageToolbar/hooks/useImageToolbarData.ts new file mode 100644 index 0000000000..bdb7e507c3 --- /dev/null +++ b/packages/react/src/components/ImageToolbar/hooks/useImageToolbarData.ts @@ -0,0 +1,33 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + ImageToolbarData, + InlineContentSchema, + SpecificBlock, + StyleSchema, +} from "@blocknote/core"; +import { useEffect, useState } from "react"; +import { UiComponentData } from "../../../components-shared/UiComponentTypes"; + +export function useImageToolbarData< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>( + editor: BlockNoteEditor +): UiComponentData, "imageToolbar"> { + const [block, setBlock] = useState>(); + + useEffect(() => { + return editor.imageToolbar.onDataUpdate((data) => { + setBlock(data.block); + }); + }, [editor]); + + return { + block: block!, + }; +} diff --git a/packages/react/src/components/ImageToolbar/hooks/useImageToolbarPosition.ts b/packages/react/src/components/ImageToolbar/hooks/useImageToolbarPosition.ts new file mode 100644 index 0000000000..46174dca5e --- /dev/null +++ b/packages/react/src/components/ImageToolbar/hooks/useImageToolbarPosition.ts @@ -0,0 +1,59 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { + flip, + offset, + useFloating, + useTransitionStyles, +} from "@floating-ui/react"; +import { useEffect, useRef, useState } from "react"; +import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; + +export function useImageToolbarPosition< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(editor: BlockNoteEditor): UiComponentPosition { + const [show, setShow] = useState(false); + const referencePos = useRef(); + + const { refs, update, context, floatingStyles } = useFloating({ + open: show, + placement: "bottom", + middleware: [offset(10), flip()], + }); + + const { isMounted, styles } = useTransitionStyles(context); + + useEffect(() => { + return editor.imageToolbar.onPositionUpdate((position) => { + setShow(position.show); + referencePos.current = position.referencePos; + update(); + }); + }, [editor.imageToolbar, update]); + + useEffect(() => { + refs.setReference({ + getBoundingClientRect: () => referencePos.current!, + }); + }, [refs]); + + return { + isMounted: isMounted, + ref: refs.setFloating, + style: { + display: "flex", + ...styles, + ...floatingStyles, + zIndex: 5000, + }, + }; +} diff --git a/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx b/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx index ea7fba393f..f234f55486 100644 --- a/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx +++ b/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx @@ -1,11 +1,7 @@ -import { BlockSchema } from "@blocknote/core"; import { AiOutlinePlus } from "react-icons/ai"; import { SideMenuButton } from "../SideMenuButton"; -import type { SideMenuProps } from "../SideMenuPositioner"; -export const AddBlockButton = ( - props: SideMenuProps -) => ( +export const AddBlockButton = (props: { addBlock: () => void }) => ( ( - props: SideMenuProps -) => { +export const DragHandle = (props: { + editor: BlockNoteEditor; + block: Block; + blockDragStart: (event: { + dataTransfer: DataTransfer | null; + clientY: number; + }) => void; + blockDragEnd: () => void; + freezeMenu: () => void; + unfreezeMenu: () => void; + dragHandleMenu?: FC>; +}) => { const DragHandleMenu = props.dragHandleMenu || DefaultDragHandleMenu; return ( diff --git a/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx new file mode 100644 index 0000000000..6ea13d5fa4 --- /dev/null +++ b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx @@ -0,0 +1,38 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { FC } from "react"; + +import { DefaultSideMenu } from "./DefaultSideMenu"; +import { useSideMenuPosition } from "./hooks/useSideMenuPosition"; + +export const DefaultPositionedSideMenu = < + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(props: { + editor: BlockNoteEditor; + sideMenu?: FC<{ + editor: BlockNoteEditor; + }>; +}) => { + const { isMounted, ref, style } = useSideMenuPosition(props.editor); + + if (!isMounted) { + return null; + } + + const SideMenu = props.sideMenu || DefaultSideMenu; + + return ( +
+ +
+ ); +}; diff --git a/packages/react/src/components/SideMenu/DefaultSideMenu.tsx b/packages/react/src/components/SideMenu/DefaultSideMenu.tsx index 9bdf66991e..00a7c11d7f 100644 --- a/packages/react/src/components/SideMenu/DefaultSideMenu.tsx +++ b/packages/react/src/components/SideMenu/DefaultSideMenu.tsx @@ -1,20 +1,46 @@ -import { BlockSchema, InlineContentSchema } from "@blocknote/core"; +import { + BlockNoteEditor, + BlockSchema, + InlineContentSchema, +} from "@blocknote/core"; import { StyleSchema } from "@blocknote/core"; import { AddBlockButton } from "./DefaultButtons/AddBlockButton"; import { DragHandle } from "./DefaultButtons/DragHandle"; import { SideMenu } from "./SideMenu"; -import type { SideMenuProps } from "./SideMenuPositioner"; +import { useSideMenuData } from "./hooks/useSideMenuData"; +import { FC } from "react"; +import type { DragHandleMenuProps } from "./DragHandleMenu/DragHandleMenu"; export const DefaultSideMenu = < BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->( - props: SideMenuProps -) => ( - - - - -); +>(props: { + editor: BlockNoteEditor; + dragHandleMenu?: FC>; +}) => { + const { + block, + addBlock, + blockDragStart, + blockDragEnd, + freezeMenu, + unfreezeMenu, + } = useSideMenuData(props.editor); + + return ( + + + + + ); +}; diff --git a/packages/react/src/components/SideMenu/SideMenuPositioner.tsx b/packages/react/src/components/SideMenu/SideMenuPositioner.tsx deleted file mode 100644 index fc4198d305..0000000000 --- a/packages/react/src/components/SideMenu/SideMenuPositioner.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { - Block, - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - InlineContentSchema, - SideMenuProsemirrorPlugin, - StyleSchema, -} from "@blocknote/core"; -import { useFloating, useTransitionStyles } from "@floating-ui/react"; -import { FC, useEffect, useRef, useState } from "react"; - -import { DefaultSideMenu } from "./DefaultSideMenu"; -import { DragHandleMenuProps } from "./DragHandleMenu/DragHandleMenu"; - -export type SideMenuProps< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema -> = Pick< - SideMenuProsemirrorPlugin, - "blockDragStart" | "blockDragEnd" | "addBlock" | "freezeMenu" | "unfreezeMenu" -> & { - block: Block; - editor: BlockNoteEditor; - dragHandleMenu?: FC>; -}; - -export const SideMenuPositioner = < - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->(props: { - editor: BlockNoteEditor; - sideMenu?: FC>; -}) => { - const [show, setShow] = useState(false); - const [block, setBlock] = useState>(); - - const referencePos = useRef(); - - const { refs, update, context, floatingStyles } = useFloating({ - open: show, - placement: "left", - }); - - const { isMounted, styles } = useTransitionStyles(context); - - useEffect(() => { - return props.editor.sideMenu.onUpdate((sideMenuState) => { - setShow(sideMenuState.show); - setBlock(sideMenuState.block); - - referencePos.current = sideMenuState.referencePos; - - update(); - }); - }, [props.editor, update]); - - useEffect(() => { - refs.setReference({ - getBoundingClientRect: () => referencePos.current!, - }); - }, [refs]); - - if (!block || !isMounted) { - return null; - } - - const SideMenu = props.sideMenu || DefaultSideMenu; - - return ( -
- -
- ); -}; diff --git a/packages/react/src/components/SideMenu/hooks/useSideMenuData.ts b/packages/react/src/components/SideMenu/hooks/useSideMenuData.ts new file mode 100644 index 0000000000..58c68c13bc --- /dev/null +++ b/packages/react/src/components/SideMenu/hooks/useSideMenuData.ts @@ -0,0 +1,38 @@ +import { + Block, + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + SideMenuData, + StyleSchema, +} from "@blocknote/core"; +import { useEffect, useState } from "react"; +import { UiComponentData } from "../../../components-shared/UiComponentTypes"; + +export function useSideMenuData< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>( + editor: BlockNoteEditor +): UiComponentData, "sideMenu"> { + const [block, setBlock] = useState>(); + + useEffect(() => { + return editor.sideMenu.onDataUpdate((data) => { + setBlock(data.block); + }); + }, [editor]); + + return { + block: block!, + addBlock: editor.sideMenu.addBlock, + blockDragStart: editor.sideMenu.blockDragStart, + blockDragEnd: editor.sideMenu.blockDragEnd, + freezeMenu: editor.sideMenu.freezeMenu, + unfreezeMenu: editor.sideMenu.unfreezeMenu, + }; +} diff --git a/packages/react/src/components/SideMenu/hooks/useSideMenuPosition.ts b/packages/react/src/components/SideMenu/hooks/useSideMenuPosition.ts new file mode 100644 index 0000000000..39526ed74c --- /dev/null +++ b/packages/react/src/components/SideMenu/hooks/useSideMenuPosition.ts @@ -0,0 +1,53 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { useFloating, useTransitionStyles } from "@floating-ui/react"; +import { useEffect, useRef, useState } from "react"; +import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; + +export function useSideMenuPosition< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(editor: BlockNoteEditor): UiComponentPosition { + const [show, setShow] = useState(false); + const referencePos = useRef(); + + const { refs, update, context, floatingStyles } = useFloating({ + open: show, + placement: "left", + }); + + const { isMounted, styles } = useTransitionStyles(context); + + useEffect(() => { + return editor.sideMenu.onPositionUpdate((position) => { + setShow(position.show); + referencePos.current = position.referencePos; + update(); + }); + }, [editor.sideMenu, update]); + + useEffect(() => { + refs.setReference({ + getBoundingClientRect: () => referencePos.current!, + }); + }, [refs]); + + return { + isMounted: isMounted, + ref: refs.setFloating, + style: { + display: "flex", + ...styles, + ...floatingStyles, + zIndex: 1000, + }, + }; +} diff --git a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx new file mode 100644 index 0000000000..58954d714e --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx @@ -0,0 +1,52 @@ +import { FC } from "react"; +import { + BlockNoteEditor, + BlockSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; + +import { useSuggestionMenuPosition } from "./hooks/useSuggestionMenuPosition"; +import { DefaultSuggestionMenu } from "./DefaultSuggestionMenu"; +import { SuggestionMenuComponentProps } from "./MantineDefaults/MantineSuggestionMenu"; +import { SuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; + +export function DefaultPositionedSuggestionMenu< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, + Item extends { + name: string; + execute: () => void; + } = SuggestionMenuItemProps +>(props: { + editor: BlockNoteEditor; + triggerCharacter?: string; + getItems?: ( + query: string, + closeMenu: () => void, + clearQuery: () => void + ) => Promise; + suggestionMenuComponent?: FC>; +}) { + const { editor, triggerCharacter, getItems, suggestionMenuComponent } = props; + + const { isMounted, ref, style } = useSuggestionMenuPosition( + editor, + triggerCharacter || "/" + ); + + if (!isMounted) { + return null; + } + + return ( +
+ +
+ ); +} diff --git a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx index 2212b81bd1..870aa2869e 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx @@ -6,18 +6,18 @@ import { StyleSchema, } from "@blocknote/core"; -import { SuggestionMenuItemProps } from "./MantineSuggestionMenuItem"; +import { useSuggestionMenuData } from "./hooks/useSuggestionMenuData"; import { useLoadSuggestionMenuItems } from "./hooks/useLoadSuggestionMenuItems"; import { useCloseSuggestionMenuNoItems } from "./hooks/useCloseSuggestionMenuNoItems"; import { useSuggestionMenuKeyboardNavigation } from "./hooks/useSuggestionMenuKeyboardNavigation"; -import { useSuggestionMenu } from "../../hooks/useSuggestionMenu"; import { defaultGetItems } from "./defaultGetItems"; import { MantineSuggestionMenu, - SuggestionMenuProps, -} from "./MantineSuggestionMenu"; + SuggestionMenuComponentProps, +} from "./MantineDefaults/MantineSuggestionMenu"; +import { SuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; -export function DefaultPositionedSuggestionMenu< +export type SuggestionMenuProps< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema, @@ -25,7 +25,7 @@ export function DefaultPositionedSuggestionMenu< name: string; execute: () => void; } = SuggestionMenuItemProps ->(props: { +> = { editor: BlockNoteEditor; triggerCharacter?: string; getItems?: ( @@ -33,41 +33,9 @@ export function DefaultPositionedSuggestionMenu< closeMenu: () => void, clearQuery: () => void ) => Promise; - suggestionMenuComponent?: FC>; -}) { - const { editor, triggerCharacter, getItems, suggestionMenuComponent } = props; - - const { isMounted, suggestionMenuProps, positionerProps } = useSuggestionMenu( - editor, - triggerCharacter || "/" - ); - - if (!isMounted) { - return null; - } + suggestionMenuComponent?: FC>; +}; - return ( -
- -
- ); -} - -// TODO: The reason these 2 components are split is because the hooks in -// `DefaultSuggestionMenu` assume that the menu is open. Therefore, they should -// only be run when the menu is open (you cannot conditionally run hooks). -// We could add a `show` param to each hook, but I feel like that is less -// "React-ish" than splitting the components. I think that it might be an issue -// that the menu plugins currently send both position and state data in the -// same update, as we can't separate `useSuggestionMenu` into 2 hooks, one for -// state and one for position. -// TODO: Make renderItems accept any item type if getItems also returns an -// arbitrary data type. export function DefaultSuggestionMenu< BSchema extends BlockSchema, I extends InlineContentSchema, @@ -76,26 +44,13 @@ export function DefaultSuggestionMenu< name: string; execute: () => void; } = SuggestionMenuItemProps ->(props: { - editor: BlockNoteEditor; - query: string; - closeMenu: () => void; - clearQuery: () => void; - getItems?: ( - query: string, - closeMenu: () => void, - clearQuery: () => void - ) => Promise; - suggestionMenuComponent?: FC>; -}) { - const { +>(props: SuggestionMenuProps) { + const { editor, triggerCharacter, getItems, suggestionMenuComponent } = props; + + const { query, closeMenu, clearQuery } = useSuggestionMenuData( editor, - query, - closeMenu, - clearQuery, - getItems, - suggestionMenuComponent, - } = props; + triggerCharacter || "/" + ); const getItemsForLoading = useMemo<(query: string) => Promise>( () => (query: string) => @@ -120,11 +75,11 @@ export function DefaultSuggestionMenu< closeMenu ); - const SuggestionMenu: FC> = + const SuggestionMenuComponent: FC> = suggestionMenuComponent || MantineSuggestionMenu; return ( - = { +export type SuggestionMenuComponentProps = { items: T[]; loadingState: "loading-initial" | "loading" | "loaded"; selectedIndex: number; }; export function MantineSuggestionMenu( - props: SuggestionMenuProps + props: SuggestionMenuComponentProps ) { const { items, loadingState, selectedIndex } = props; diff --git a/packages/react/src/components/SuggestionMenu/MantineSuggestionMenuItem.tsx b/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenuItem.tsx similarity index 100% rename from packages/react/src/components/SuggestionMenu/MantineSuggestionMenuItem.tsx rename to packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenuItem.tsx diff --git a/packages/react/src/components/SuggestionMenu/defaultGetItems.tsx b/packages/react/src/components/SuggestionMenu/defaultGetItems.tsx index 3471ad79f8..1e1f8dfe05 100644 --- a/packages/react/src/components/SuggestionMenu/defaultGetItems.tsx +++ b/packages/react/src/components/SuggestionMenu/defaultGetItems.tsx @@ -23,7 +23,7 @@ import { RiText, } from "react-icons/ri"; -import { SuggestionMenuItemProps } from "./MantineSuggestionMenuItem"; +import { SuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; // Sets the editor's text cursor position to the next content editable block, // so either a block with inline content or a table. The last block is always a @@ -88,10 +88,7 @@ export function insertOrUpdateBlock< return insertedBlock; } -// TODO: Not sure on the return type of this. I think it's nice that the items -// can just be plugged as props into SuggestionMenuLabel and SuggestionMenuItem -// components, but the labels make the keyboard selection code more complex. -// TODO: Also probably want an easier way of customizing the items list. +// TODO: Probably want an easier way of customizing the items list. export async function defaultGetItems< BSchema extends BlockSchema = DefaultBlockSchema, I extends InlineContentSchema = DefaultInlineContentSchema, diff --git a/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts b/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts index d8cae06427..4f526f622f 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from "react"; -import { SuggestionMenuItemProps } from "../MantineSuggestionMenuItem"; +import { SuggestionMenuItemProps } from "../MantineDefaults/MantineSuggestionMenuItem"; // Hook which closes the suggestion after a certain number of consecutive // invalid queries are made. An invalid query is one which returns no items, and diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuData.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuData.ts new file mode 100644 index 0000000000..64d3670474 --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuData.ts @@ -0,0 +1,30 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { useEffect, useState } from "react"; + +export function useSuggestionMenuData< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(editor: BlockNoteEditor, triggerCharacter: string) { + const [query, setQuery] = useState(""); + + useEffect(() => { + return editor.suggestionMenus.onDataUpdate(triggerCharacter, (data) => { + setQuery(data.query); + }); + }, [editor.suggestionMenus, triggerCharacter]); + + return { + query: query, + closeMenu: editor.suggestionMenus.closeMenu, + clearQuery: editor.suggestionMenus.clearQuery, + }; +} diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts index edef4b737c..649c9eb021 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts @@ -1,6 +1,6 @@ import { BlockNoteEditor } from "@blocknote/core"; import { useEffect, useState } from "react"; -import { SuggestionMenuItemProps } from "../MantineSuggestionMenuItem"; +import { SuggestionMenuItemProps } from "../MantineDefaults/MantineSuggestionMenuItem"; // Hook which handles keyboard navigation of a suggestion menu. Arrow keys are // used to select a menu item, enter to execute it, and escape to close the diff --git a/packages/react/src/hooks/useSuggestionMenu.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuPosition.ts similarity index 69% rename from packages/react/src/hooks/useSuggestionMenu.ts rename to packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuPosition.ts index 23eb0bd9ca..6cd9752525 100644 --- a/packages/react/src/hooks/useSuggestionMenu.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuPosition.ts @@ -16,14 +16,12 @@ import { } from "@floating-ui/react"; import { useEffect, useRef, useState } from "react"; -// TODO: maybe separate the positioning part (floating ui) -export function useSuggestionMenu< +export function useSuggestionMenuPosition< BSchema extends BlockSchema = DefaultBlockSchema, I extends InlineContentSchema = DefaultInlineContentSchema, S extends StyleSchema = DefaultStyleSchema >(editor: BlockNoteEditor, triggerCharacter: string) { const [show, setShow] = useState(false); - const [query, setQuery] = useState(""); const referencePos = useRef(); @@ -48,14 +46,11 @@ export function useSuggestionMenu< const { isMounted, styles } = useTransitionStyles(context); useEffect(() => { - return editor.suggestionMenus.onUpdate( + return editor.suggestionMenus.onPositionUpdate( triggerCharacter, - (suggestionsMenuState) => { - setShow(suggestionsMenuState.show); - setQuery(suggestionsMenuState.query); - - referencePos.current = suggestionsMenuState.referencePos; - + (position) => { + setShow(position.show); + referencePos.current = position.referencePos; update(); } ); @@ -69,19 +64,12 @@ export function useSuggestionMenu< return { isMounted: isMounted, - suggestionMenuProps: { - query, - closeMenu: editor.suggestionMenus.closeMenu, - clearQuery: editor.suggestionMenus.clearQuery, - }, - positionerProps: { - ref: refs.setFloating, - styles: { - display: "flex", - ...styles, - ...floatingStyles, - zIndex: 2000, - }, + ref: refs.setFloating, + style: { + display: "flex", + ...styles, + ...floatingStyles, + zIndex: 2000, }, }; } diff --git a/packages/react/src/components/TableHandles/hooks/useTableHandlesData.ts b/packages/react/src/components/TableHandles/hooks/useTableHandlesData.ts new file mode 100644 index 0000000000..476d92ea91 --- /dev/null +++ b/packages/react/src/components/TableHandles/hooks/useTableHandlesData.ts @@ -0,0 +1,55 @@ +import { + BlockFromConfigNoChildren, + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, + TableHandlesData, +} from "@blocknote/core"; +import { useEffect, useState } from "react"; +import { UiComponentData } from "../../../components-shared/UiComponentTypes"; + +export function useTableHandlesData< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>( + editor: BlockNoteEditor +): UiComponentData, "tableHandles"> { + const [block, setBlock] = + useState>(); + const [rowIndex, setRowIndex] = useState(); + const [colIndex, setColIndex] = useState(); + const [draggedCellOrientation, setDraggedCellOrientation] = useState< + "row" | "col" | undefined + >(undefined); + + useEffect(() => { + return editor.tableHandles!.onDataUpdate((data) => { + setBlock(data.block); + setRowIndex(data.rowIndex); + setColIndex(data.colIndex); + + if (data.draggingState) { + setDraggedCellOrientation(data.draggingState.draggedCellOrientation); + } else { + setDraggedCellOrientation(undefined); + } + }); + }, [editor]); + + return { + block: block!, + rowIndex: rowIndex!, + colIndex: colIndex!, + draggingState: draggedCellOrientation, + rowDragStart: editor.tableHandles!.rowDragStart, + colDragStart: editor.tableHandles!.colDragStart, + dragEnd: editor.tableHandles!.dragEnd, + freezeHandles: editor.tableHandles!.freezeHandles, + unfreezeHandles: editor.tableHandles!.unfreezeHandles, + } as any; +} diff --git a/packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts b/packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts new file mode 100644 index 0000000000..c5700c81bb --- /dev/null +++ b/packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts @@ -0,0 +1,139 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; +import { useEffect, useRef, useState } from "react"; +import { offset, useFloating, useTransitionStyles } from "@floating-ui/react"; + +export function useTableHandlesPosition< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>( + editor: BlockNoteEditor +): { + rowHandle: UiComponentPosition; + colHandle: UiComponentPosition; +} { + const [show, setShow] = useState(false); + + const [draggedCellOrientation, setDraggedCellOrientation] = useState< + "row" | "col" | undefined + >(undefined); + const [mousePos, setMousePos] = useState(); + + const [, setForceUpdate] = useState(0); + + const referencePosCell = useRef(); + const referencePosTable = useRef(); + + const rowFloating = useFloating({ + open: show, + placement: "left", + middleware: [offset(-10)], + }); + const colFloating = useFloating({ + open: show, + placement: "top", + middleware: [offset(-12)], + }); + + const rowTransitionStyles = useTransitionStyles(rowFloating.context); + const colTransitionStyles = useTransitionStyles(colFloating.context); + + useEffect(() => { + return editor.tableHandles!.onPositionUpdate((position) => { + // console.log("update", state.draggingState); + setShow(position.show); + setForceUpdate(Math.random()); + + if (position.draggingState) { + setDraggedCellOrientation( + position.draggingState.draggedCellOrientation + ); + setMousePos(position.draggingState.mousePos); + } else { + setDraggedCellOrientation(undefined); + setMousePos(undefined); + } + + referencePosCell.current = position.referencePosCell; + referencePosTable.current = position.referencePosTable; + rowFloating.update(); + colFloating.update(); + }); + }, [colFloating, editor.tableHandles, rowFloating]); + + useEffect(() => { + rowFloating.refs.setReference({ + getBoundingClientRect: () => { + if (draggedCellOrientation === "row") { + return new DOMRect( + referencePosTable.current!.x, + mousePos!, + referencePosTable.current!.width, + 0 + ); + } + + return new DOMRect( + referencePosTable.current!.x, + referencePosCell.current!.y, + referencePosTable.current!.width, + referencePosCell.current!.height + ); + }, + }); + }, [draggedCellOrientation, mousePos, rowFloating.refs]); + + useEffect(() => { + colFloating.refs.setReference({ + getBoundingClientRect: () => { + if (draggedCellOrientation === "col") { + return new DOMRect( + mousePos!, + referencePosTable.current!.y, + 0, + referencePosTable.current!.height + ); + } + + return new DOMRect( + referencePosCell.current!.x, + referencePosTable.current!.y, + referencePosCell.current!.width, + referencePosTable.current!.height + ); + }, + }); + }, [colFloating.refs, draggedCellOrientation, mousePos]); + + return { + rowHandle: { + isMounted: rowTransitionStyles.isMounted, + ref: rowFloating.refs.setFloating, + style: { + display: "flex", + ...rowTransitionStyles.styles, + ...rowFloating.floatingStyles, + zIndex: 10000, + }, + }, + colHandle: { + isMounted: colTransitionStyles.isMounted, + ref: colFloating.refs.setFloating, + style: { + display: "flex", + ...colTransitionStyles.styles, + ...colFloating.floatingStyles, + zIndex: 10000, + }, + }, + }; +} diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 65f8d1a9e9..9799f15be9 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -1,8 +1,8 @@ -import { FormattingToolbarPositioner } from "../components/FormattingToolbar/FormattingToolbarPositioner"; -import { HyperlinkToolbarPositioner } from "../components/HyperlinkToolbar/HyperlinkToolbarPositioner"; -import { DefaultPositionedSuggestionMenu } from "../components/SuggestionMenu/DefaultSuggestionMenu"; -import { SideMenuPositioner } from "../components/SideMenu/SideMenuPositioner"; -import { ImageToolbarPositioner } from "../components/ImageToolbar/ImageToolbarPositioner"; +import { DefaultPositionedFormattingToolbar } from "../components/FormattingToolbar/DefaultPositionedFormattingToolbar"; +import { DefaultPositionedHyperlinkToolbar } from "../components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar"; +import { DefaultPositionedSuggestionMenu } from "../components/SuggestionMenu/DefaultPositionedSuggestionMenu"; +import { DefaultPositionedSideMenu } from "../components/SideMenu/DefaultPositionedSideMenu"; +import { DefaultPositionedImageToolbar } from "../components/ImageToolbar/DefaultPositionedImageToolbar"; import { TableHandlesPositioner } from "../components/TableHandles/TableHandlePositioner"; import { BlockNoteEditor, @@ -27,17 +27,19 @@ export function BlockNoteDefaultUI< return ( <> {props.formattingToolbar !== false && ( - + )} {props.hyperlinkToolbar !== false && ( - + )} {props.slashMenu !== false && ( )} - {props.sideMenu !== false && } + {props.sideMenu !== false && ( + + )} {props.imageToolbar !== false && ( - + )} {props.editor.blockSchema.table && props.tableHandles !== false && ( diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index d0e698c136..c9482a341f 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -4,6 +4,7 @@ export * from "./editor/BlockNoteTheme"; export * from "./editor/BlockNoteView"; export * from "./editor/defaultThemes"; +export * from "./components/FormattingToolbar/hooks/useFormattingToolbarPosition"; export * from "./components/FormattingToolbar/DefaultButtons/ColorStyleButton"; export * from "./components/FormattingToolbar/DefaultButtons/CreateLinkButton"; export * from "./components/FormattingToolbar/DefaultButtons/ImageCaptionButton"; @@ -13,16 +14,21 @@ export * from "./components/FormattingToolbar/DefaultButtons/TextAlignButton"; export * from "./components/FormattingToolbar/DefaultButtons/ToggledStyleButton"; export * from "./components/FormattingToolbar/DefaultDropdowns/BlockTypeDropdown"; export * from "./components/FormattingToolbar/DefaultFormattingToolbar"; -export * from "./components/FormattingToolbar/FormattingToolbarPositioner"; +export * from "./components/FormattingToolbar/DefaultPositionedFormattingToolbar"; -export * from "./components/HyperlinkToolbar/HyperlinkToolbarPositioner"; +export * from "./components/HyperlinkToolbar/hooks/useHyperlinkToolbarData"; +export * from "./components/HyperlinkToolbar/hooks/useHyperlinkToolbarPosition"; +export * from "./components/HyperlinkToolbar/DefaultHyperlinkToolbar"; +export * from "./components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar"; +export * from "./components/SideMenu/hooks/useSideMenuData"; +export * from "./components/SideMenu/hooks/useSideMenuPosition"; export * from "./components/SideMenu/DefaultButtons/AddBlockButton"; export * from "./components/SideMenu/DefaultButtons/DragHandle"; -export * from "./components/SideMenu/DefaultSideMenu"; export * from "./components/SideMenu/SideMenu"; export * from "./components/SideMenu/SideMenuButton"; -export * from "./components/SideMenu/SideMenuPositioner"; +export * from "./components/SideMenu/DefaultSideMenu"; +export * from "./components/SideMenu/DefaultPositionedSideMenu"; export * from "./components/SideMenu/DragHandleMenu/DefaultButtons/BlockColorsButton"; export * from "./components/SideMenu/DragHandleMenu/DefaultButtons/RemoveBlockButton"; @@ -30,17 +36,24 @@ export * from "./components/SideMenu/DragHandleMenu/DefaultDragHandleMenu"; export * from "./components/SideMenu/DragHandleMenu/DragHandleMenu"; export * from "./components/SideMenu/DragHandleMenu/DragHandleMenuItem"; -export * from "./components/SuggestionMenu/MantineSuggestionMenu"; -export * from "./components/SuggestionMenu/MantineSuggestionMenuItem"; -export * from "./components/SuggestionMenu/DefaultSuggestionMenu"; -export * from "./components/SuggestionMenu/defaultGetItems"; -export * from "./components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems"; +export * from "./components/SuggestionMenu/hooks/useSuggestionMenuData"; +export * from "./components/SuggestionMenu/hooks/useSuggestionMenuPosition"; export * from "./components/SuggestionMenu/hooks/useLoadSuggestionMenuItems"; +export * from "./components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems"; export * from "./components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation"; +export * from "./components/SuggestionMenu/MantineDefaults/MantineSuggestionMenu"; +export * from "./components/SuggestionMenu/MantineDefaults/MantineSuggestionMenuItem"; +export * from "./components/SuggestionMenu/defaultGetItems"; +export * from "./components/SuggestionMenu/DefaultSuggestionMenu"; +export * from "./components/SuggestionMenu/DefaultPositionedSuggestionMenu"; +export * from "./components/ImageToolbar/hooks/useImageToolbarData"; +export * from "./components/ImageToolbar/hooks/useImageToolbarPosition"; export * from "./components/ImageToolbar/DefaultImageToolbar"; -export * from "./components/ImageToolbar/ImageToolbarPositioner"; +export * from "./components/ImageToolbar/DefaultPositionedImageToolbar"; +export * from "./components/TableHandles/hooks/useTableHandlesData"; +export * from "./components/TableHandles/hooks/useTableHandlesPosition"; export * from "./components/TableHandles/DefaultTableHandle"; export * from "./components/TableHandles/TableHandlePositioner"; @@ -55,7 +68,6 @@ export * from "./hooks/useEditorContentChange"; export * from "./hooks/useEditorForceUpdate"; export * from "./hooks/useEditorSelectionChange"; export * from "./hooks/useSelectedBlocks"; -export * from "./hooks/useSuggestionMenu"; export * from "./schema/ReactBlockSpec"; export * from "./schema/ReactInlineContentSpec"; From e196d3f255dbb5e6540bda806804d43a13872e22 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 14 Feb 2024 17:11:06 +0100 Subject: [PATCH 2/7] Replaced `data`/`position` hooks with refactored general hooks --- .../FormattingToolbarPlugin.ts | 58 ++++---- .../HyperlinkToolbarPlugin.ts | 96 +++++-------- .../ImageToolbar/ImageToolbarPlugin.ts | 91 +++++-------- .../src/extensions/SideMenu/SideMenuPlugin.ts | 128 +++++++----------- .../SuggestionMenu/SuggestionPlugin.ts | 82 ++++------- .../DefaultPositionedUiElement.tsx | 59 ++++++++ .../DefaultFormattingToolbar.tsx | 11 +- .../DefaultPositionedFormattingToolbar.tsx | 73 +++++++++- .../hooks/useFormattingToolbarPosition.ts | 105 -------------- .../DefaultHyperlinkToolbar.tsx | 25 ++-- .../DefaultPositionedHyperlinkToolbar.tsx | 37 ++++- .../hooks/useHyperlinkToolbarData.ts | 39 ------ .../hooks/useHyperlinkToolbarPosition.ts | 59 -------- .../ImageToolbar/DefaultImageToolbar.tsx | 31 +++-- .../DefaultPositionedImageToolbar.tsx | 34 +++-- .../ImageToolbar/hooks/useImageToolbarData.ts | 33 ----- .../hooks/useImageToolbarPosition.ts | 59 -------- .../SideMenu/DefaultPositionedSideMenu.tsx | 29 +++- .../components/SideMenu/DefaultSideMenu.tsx | 47 +++---- .../SideMenu/hooks/useSideMenuData.ts | 38 ------ .../SideMenu/hooks/useSideMenuPosition.ts | 53 -------- .../DefaultPositionedSuggestionMenu.tsx | 53 ++++++-- .../SuggestionMenu/DefaultSuggestionMenu.tsx | 28 ++-- .../hooks/useSuggestionMenuData.ts | 30 ---- .../hooks/useSuggestionMenuPosition.ts | 75 ---------- packages/react/src/hooks/useUiElement.ts | 15 ++ .../react/src/hooks/useUiElementPosition.ts | 45 ++++++ packages/react/src/index.ts | 9 -- 28 files changed, 560 insertions(+), 882 deletions(-) create mode 100644 packages/react/src/components-shared/DefaultPositionedUiElement.tsx delete mode 100644 packages/react/src/components/FormattingToolbar/hooks/useFormattingToolbarPosition.ts delete mode 100644 packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarData.ts delete mode 100644 packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarPosition.ts delete mode 100644 packages/react/src/components/ImageToolbar/hooks/useImageToolbarData.ts delete mode 100644 packages/react/src/components/ImageToolbar/hooks/useImageToolbarPosition.ts delete mode 100644 packages/react/src/components/SideMenu/hooks/useSideMenuData.ts delete mode 100644 packages/react/src/components/SideMenu/hooks/useSideMenuPosition.ts delete mode 100644 packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuData.ts delete mode 100644 packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuPosition.ts create mode 100644 packages/react/src/hooks/useUiElement.ts create mode 100644 packages/react/src/hooks/useUiElementPosition.ts diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index 6597a60d2a..5a04c3a39d 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -3,13 +3,15 @@ import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { EventEmitter } from "../../util/EventEmitter"; +export type FormattingToolbarState = UiElementPosition; + export class FormattingToolbarView { - private position?: UiElementPosition; - private readonly updatePosition: () => void; + public state?: FormattingToolbarState; + public emitUpdate: () => void; public preventHide = false; public preventShow = false; @@ -29,16 +31,16 @@ export class FormattingToolbarView { StyleSchema >, private readonly pmView: EditorView, - updatePosition: (position: UiElementPosition) => void + emitUpdate: (state: FormattingToolbarState) => void ) { - this.updatePosition = () => { - if (!this.position) { + this.emitUpdate = () => { + if (!this.state) { throw new Error( "Attempting to update uninitialized formatting toolbar" ); } - updatePosition(this.position); + emitUpdate(this.state); }; pmView.dom.addEventListener("mousedown", this.viewMousedownHandler); @@ -63,9 +65,9 @@ export class FormattingToolbarView { // For dragging the whole editor. dragHandler = () => { - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } }; @@ -96,16 +98,16 @@ export class FormattingToolbarView { return; } - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } }; scrollHandler = () => { - if (this.position?.show) { - this.position.referencePos = this.getSelectionBoundingBox(); - this.updatePosition(); + if (this.state?.show) { + this.state.referencePos = this.getSelectionBoundingBox(); + this.emitUpdate(); } }; @@ -143,24 +145,24 @@ export class FormattingToolbarView { !this.preventShow && (shouldShow || this.preventHide) ) { - this.position = { + this.state = { show: true, referencePos: this.getSelectionBoundingBox(), }; - this.updatePosition(); + this.emitUpdate(); return; } // Checks if menu should be hidden. if ( - this.position?.show && + this.state?.show && !this.preventHide && (!shouldShow || this.preventShow || !this.editor.isEditable) ) { - this.position.show = false; - this.updatePosition(); + this.state.show = false; + this.emitUpdate(); return; } @@ -212,19 +214,15 @@ export class FormattingToolbarProsemirrorPlugin extends EventEmitter { this.plugin = new Plugin({ key: formattingToolbarPluginKey, view: (editorView) => { - this.view = new FormattingToolbarView( - editor, - editorView, - (position) => { - this.emit("update position", position); - } - ); + this.view = new FormattingToolbarView(editor, editorView, (state) => { + this.emit("update", state); + }); return this.view; }, }); } - public onPositionUpdate(callback: (position: UiElementPosition) => void) { - return this.on("update position", callback); + public onUpdate(callback: (state: FormattingToolbarState) => void) { + return this.on("update", callback); } } diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts index 5cc0f78a47..c0d524c0b4 100644 --- a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts @@ -2,12 +2,13 @@ import { getMarkRange, posToDOMRect, Range } from "@tiptap/core"; import { EditorView } from "@tiptap/pm/view"; import { Mark } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; + import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { EventEmitter } from "../../util/EventEmitter"; -export type HyperlinkToolbarData = { +export type HyperlinkToolbarState = UiElementPosition & { // The hovered hyperlink's URL, and the text it's displayed with in the // editor. url: string; @@ -15,10 +16,8 @@ export type HyperlinkToolbarData = { }; class HyperlinkToolbarView { - private data?: HyperlinkToolbarData; - private position?: UiElementPosition; - private readonly updateData: () => void; - private readonly updatePosition: () => void; + public state?: HyperlinkToolbarState; + public emitUpdate: () => void; menuUpdateTimer: ReturnType | undefined; startMenuUpdateTimer: () => void; @@ -36,23 +35,14 @@ class HyperlinkToolbarView { constructor( private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, - updateData: (data: HyperlinkToolbarData) => void, - updatePosition: (position: UiElementPosition) => void + emitUpdate: (state: HyperlinkToolbarState) => void ) { - this.updateData = () => { - if (!this.data) { + this.emitUpdate = () => { + if (!this.state) { throw new Error("Attempting to update uninitialized hyperlink toolbar"); } - updateData(this.data); - }; - - this.updatePosition = () => { - if (!this.position) { - throw new Error("Attempting to update uninitialized hyperlink toolbar"); - } - - updatePosition(this.position); + emitUpdate(this.state); }; this.startMenuUpdateTimer = () => { @@ -133,22 +123,22 @@ class HyperlinkToolbarView { editorWrapper.contains(event.target as Node) ) ) { - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } } }; scrollHandler = () => { if (this.hyperlinkMark !== undefined) { - if (this.position?.show) { - this.position.referencePos = posToDOMRect( + if (this.state?.show) { + this.state.referencePos = posToDOMRect( this.pmView, this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to ); - this.updatePosition(); + this.emitUpdate(); } } }; @@ -167,9 +157,9 @@ class HyperlinkToolbarView { this.pmView.dispatch(tr); this.pmView.focus(); - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } } @@ -185,9 +175,9 @@ class HyperlinkToolbarView { ); this.pmView.focus(); - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } } @@ -241,35 +231,32 @@ class HyperlinkToolbarView { } if (this.hyperlinkMark && this.editor.isEditable) { - this.data = { - url: this.hyperlinkMark!.attrs.href, - text: this.pmView.state.doc.textBetween( - this.hyperlinkMarkRange!.from, - this.hyperlinkMarkRange!.to - ), - }; - this.position = { + this.state = { show: true, referencePos: posToDOMRect( this.pmView, this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to ), + url: this.hyperlinkMark!.attrs.href, + text: this.pmView.state.doc.textBetween( + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.to + ), }; - this.updateData(); - this.updatePosition(); + this.emitUpdate(); return; } // Hides menu. if ( - this.position?.show && + this.state?.show && prevHyperlinkMark && (!this.hyperlinkMark || !this.editor.isEditable) ) { - this.position.show = false; - this.updatePosition(); + this.state.show = false; + this.emitUpdate(); return; } @@ -299,27 +286,16 @@ export class HyperlinkToolbarProsemirrorPlugin< this.plugin = new Plugin({ key: hyperlinkToolbarPluginKey, view: (editorView) => { - this.view = new HyperlinkToolbarView( - editor, - editorView, - (data) => { - this.emit("update data", data); - }, - (position) => { - this.emit("update position", position); - } - ); + this.view = new HyperlinkToolbarView(editor, editorView, (state) => { + this.emit("update", state); + }); return this.view; }, }); } - public onDataUpdate(callback: (state: HyperlinkToolbarData) => void) { - return this.on("update data", callback); - } - - public onPositionUpdate(callback: (state: UiElementPosition) => void) { - return this.on("update position", callback); + public onUpdate(callback: (state: HyperlinkToolbarState) => void) { + return this.on("update", callback); } /** diff --git a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts index 72d129434a..abc9b0bee7 100644 --- a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +++ b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts @@ -1,7 +1,6 @@ import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { EventEmitter } from "../../util/EventEmitter"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { BlockSchema, @@ -10,12 +9,13 @@ import { StyleSchema, } from "../../schema"; import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; +import { EventEmitter } from "../../util/EventEmitter"; -export type ImageToolbarData< +export type ImageToolbarState< B extends BlockSchema, I extends InlineContentSchema, - S extends StyleSchema = StyleSchema -> = { + S extends StyleSchema +> = UiElementPosition & { block: SpecificBlock; }; @@ -24,33 +24,22 @@ export class ImageToolbarView< I extends InlineContentSchema, S extends StyleSchema > { - private data?: ImageToolbarData; - private position?: UiElementPosition; - private readonly updateData: () => void; - private readonly updatePosition: () => void; + public state?: ImageToolbarState; + public emitUpdate: () => void; public prevWasEditable: boolean | null = null; constructor( private readonly pluginKey: PluginKey, private readonly pmView: EditorView, - updateData: (data: ImageToolbarData) => void, - updatePosition: (position: UiElementPosition) => void + emitUpdate: (state: ImageToolbarState) => void ) { - this.updateData = () => { - if (!this.data) { - throw new Error("Attempting to update uninitialized image toolbar"); - } - - updateData(this.data); - }; - - this.updatePosition = () => { - if (!this.position) { + this.emitUpdate = () => { + if (!this.state) { throw new Error("Attempting to update uninitialized image toolbar"); } - updatePosition(this.position); + emitUpdate(this.state); }; pmView.dom.addEventListener("mousedown", this.mouseDownHandler); @@ -63,17 +52,17 @@ export class ImageToolbarView< } mouseDownHandler = () => { - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } }; // For dragging the whole editor. dragstartHandler = () => { - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } }; @@ -93,20 +82,20 @@ export class ImageToolbarView< return; } - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } }; scrollHandler = () => { - if (this.position?.show) { + if (this.state?.show) { const blockElement = document.querySelector( - `[data-node-type="blockContainer"][data-id="${this.data!.block.id}"]` + `[data-node-type="blockContainer"][data-id="${this.state.block.id}"]` )!; - this.position.referencePos = blockElement.getBoundingClientRect(); - this.updatePosition(); + this.state.referencePos = blockElement.getBoundingClientRect(); + this.emitUpdate(); } }; @@ -115,21 +104,18 @@ export class ImageToolbarView< block: SpecificBlock; } = this.pluginKey.getState(view.state); - if (!this.position?.show && pluginState.block) { + if (!this.state?.show && pluginState.block) { const blockElement = document.querySelector( `[data-node-type="blockContainer"][data-id="${pluginState.block.id}"]` )!; - this.data = { - block: pluginState.block, - }; - this.position = { + this.state = { show: true, referencePos: blockElement.getBoundingClientRect(), + block: pluginState.block, }; - this.updateData(); - this.updatePosition(); + this.emitUpdate(); return; } @@ -138,10 +124,10 @@ export class ImageToolbarView< !view.state.selection.eq(prevState.selection) || !view.state.doc.eq(prevState.doc) ) { - if (this.position?.show) { - this.position.show = false; + if (this.state?.show) { + this.state.show = false; - this.updatePosition(); + this.emitUpdate(); } } } @@ -178,11 +164,8 @@ export class ImageToolbarProsemirrorPlugin< // editor, imageToolbarPluginKey, editorView, - (data) => { - this.emit("update data", data); - }, - (position) => { - this.emit("update position", position); + (state) => { + this.emit("update", state); } ); return this.view; @@ -205,13 +188,7 @@ export class ImageToolbarProsemirrorPlugin< }); } - public onDataUpdate( - callback: (data: ImageToolbarData) => void - ) { - return this.on("update data", callback); - } - - public onPositionUpdate(callback: (position: UiElementPosition) => void) { - return this.on("update position", callback); + public onUpdate(callback: (state: ImageToolbarState) => void) { + return this.on("update", callback); } } diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index e2797efac3..c86627956d 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -2,29 +2,30 @@ import { PluginView } from "@tiptap/pm/state"; import { Node } from "prosemirror-model"; import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { createExternalHTMLExporter } from "../../api/exporters/html/externalHTMLExporter"; -import { createInternalHTMLSerializer } from "../../api/exporters/html/internalHTMLSerializer"; -import { cleanHTMLToMarkdown } from "../../api/exporters/markdown/markdownExporter"; -import { getBlockInfoFromPos } from "../../api/getBlockInfoFromPos"; + import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { Block, BlockSchema, InlineContentSchema, StyleSchema, } from "../../schema"; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { EventEmitter } from "../../util/EventEmitter"; +import { createExternalHTMLExporter } from "../../api/exporters/html/externalHTMLExporter"; +import { createInternalHTMLSerializer } from "../../api/exporters/html/internalHTMLSerializer"; +import { cleanHTMLToMarkdown } from "../../api/exporters/markdown/markdownExporter"; +import { getBlockInfoFromPos } from "../../api/getBlockInfoFromPos"; import { MultipleNodeSelection } from "./MultipleNodeSelection"; import { suggestionMenuPluginKey } from "../SuggestionMenu/SuggestionPlugin"; let dragImageElement: Element | undefined; -export type SideMenuData< +export type SideMenuState< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema -> = { +> = UiElementPosition & { // The block that the side menu is attached to. block: Block; }; @@ -255,10 +256,8 @@ export class SideMenuView< S extends StyleSchema > implements PluginView { - private data?: SideMenuData; - private position?: UiElementPosition; - private readonly updateData: () => void; - private readonly updatePosition: () => void; + private state?: SideMenuState; + private readonly emitUpdate: (state: SideMenuState) => void; // 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 @@ -276,23 +275,14 @@ export class SideMenuView< constructor( private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, - updateData: (data: SideMenuData) => void, - updatePosition: (position: UiElementPosition) => void + emitUpdate: (state: SideMenuState) => void ) { - this.updateData = () => { - if (!this.data) { + this.emitUpdate = () => { + if (!this.state) { throw new Error("Attempting to update uninitialized side menu"); } - updateData(this.data); - }; - - this.updatePosition = () => { - if (!this.position) { - throw new Error("Attempting to update uninitialized side menu"); - } - - updatePosition(this.position); + emitUpdate(this.state); }; this.horizontalPosAnchoredAtRoot = true; @@ -387,17 +377,17 @@ export class SideMenuView< }; onKeyDown = (_event: KeyboardEvent) => { - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); } this.menuFrozen = false; }; onMouseDown = (_event: MouseEvent) => { - if (this.position && !this.position.show) { - this.position.show = true; - this.updatePosition(); + if (this.state && !this.state.show) { + this.state.show = true; + this.emitUpdate(this.state); } this.menuFrozen = false; }; @@ -439,9 +429,9 @@ export class SideMenuView< editorWrapper.contains(event.target as HTMLElement) ) ) { - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); } return; @@ -458,9 +448,9 @@ export class SideMenuView< // Closes the menu if the mouse cursor is beyond the editor vertically. if (!block || !this.editor.isEditable) { - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); } return; @@ -468,7 +458,7 @@ export class SideMenuView< // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block. if ( - this.position?.show && + this.state?.show && this.hoveredBlock?.hasAttribute("data-id") && this.hoveredBlock?.getAttribute("data-id") === block.id ) { @@ -488,12 +478,7 @@ export class SideMenuView< if (this.editor.isEditable) { const blockContentBoundingBox = blockContent.getBoundingClientRect(); - this.data = { - block: this.editor.getBlock( - this.hoveredBlock!.getAttribute("data-id")! - )!, - }; - this.position = { + this.state = { show: true, referencePos: new DOMRect( this.horizontalPosAnchoredAtRoot @@ -503,19 +488,21 @@ export class SideMenuView< blockContentBoundingBox.width, blockContentBoundingBox.height ), + block: this.editor.getBlock( + this.hoveredBlock!.getAttribute("data-id")! + )!, }; - this.updateData(); - this.updatePosition(); + this.emitUpdate(this.state); } }; onScroll = () => { - if (this.position?.show) { + if (this.state?.show) { const blockContent = this.hoveredBlock!.firstChild as HTMLElement; const blockContentBoundingBox = blockContent.getBoundingClientRect(); - this.position.referencePos = new DOMRect( + this.state.referencePos = new DOMRect( this.horizontalPosAnchoredAtRoot ? this.horizontalPosAnchor : blockContentBoundingBox.x, @@ -523,14 +510,14 @@ export class SideMenuView< blockContentBoundingBox.width, blockContentBoundingBox.height ); - this.updatePosition(); + this.emitUpdate(this.state); } }; destroy() { - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); } document.body.removeEventListener("mousemove", this.onMouseMove); document.body.removeEventListener("dragover", this.onDragOver); @@ -542,9 +529,9 @@ export class SideMenuView< } addBlock() { - if (this.position?.show) { - this.position.show = false; - this.updatePosition(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); } this.menuFrozen = true; @@ -606,7 +593,7 @@ export class SideMenuProsemirrorPlugin< I extends InlineContentSchema, S extends StyleSchema > extends EventEmitter { - private sideMenuView: SideMenuView | undefined; + public view: SideMenuView | undefined; public readonly plugin: Plugin; constructor(private readonly editor: BlockNoteEditor) { @@ -614,34 +601,23 @@ export class SideMenuProsemirrorPlugin< this.plugin = new Plugin({ key: sideMenuPluginKey, view: (editorView) => { - this.sideMenuView = new SideMenuView( - editor, - editorView, - (data) => { - this.emit("update data", data); - }, - (position) => { - this.emit("update position", position); - } - ); - return this.sideMenuView; + this.view = new SideMenuView(editor, editorView, (state) => { + this.emit("update", state); + }); + return this.view; }, }); } - public onDataUpdate(callback: (data: SideMenuData) => void) { - return this.on("update data", callback); - } - - public onPositionUpdate(callback: (position: UiElementPosition) => void) { - return this.on("update position", callback); + public onUpdate(callback: (state: SideMenuState) => void) { + return this.on("update", callback); } /** * If the block is empty, opens the slash menu. If the block has content, * creates a new block below and opens the slash menu in it. */ - addBlock = () => this.sideMenuView!.addBlock(); + addBlock = () => this.view!.addBlock(); /** * Handles drag & drop events for blocks. @@ -650,7 +626,7 @@ export class SideMenuProsemirrorPlugin< dataTransfer: DataTransfer | null; clientY: number; }) => { - this.sideMenuView!.isDragging = true; + this.view!.isDragging = true; dragStart(event, this.editor); }; @@ -663,11 +639,11 @@ export class SideMenuProsemirrorPlugin< * attached to the same block regardless of which block is hovered by the * mouse cursor. */ - freezeMenu = () => (this.sideMenuView!.menuFrozen = true); + freezeMenu = () => (this.view!.menuFrozen = true); /** * Unfreezes the side menu. When frozen, the side menu will stay * attached to the same block regardless of which block is hovered by the * mouse cursor. */ - unfreezeMenu = () => (this.sideMenuView!.menuFrozen = false); + unfreezeMenu = () => (this.view!.menuFrozen = false); } diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index 88c84a83b1..74f4e11572 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -1,14 +1,15 @@ import { findParentNode } from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; + import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; -import { EventEmitter } from "../../util/EventEmitter"; import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; +import { EventEmitter } from "../../util/EventEmitter"; const findBlock = findParentNode((node) => node.type.name === "blockContainer"); -export type SuggestionMenuData = { +export type SuggestionMenuState = UiElementPosition & { query: string; }; @@ -17,49 +18,35 @@ class SuggestionMenuView< I extends InlineContentSchema, S extends StyleSchema > { - private data?: SuggestionMenuData; - private position?: UiElementPosition; - public updateData: (triggerCharacter: string) => void; - public updatePosition: (triggerCharacter: string) => void; + private state?: SuggestionMenuState; + public emitUpdate: (triggerCharacter: string) => void; pluginState: SuggestionPluginState; constructor( private readonly editor: BlockNoteEditor, - updateData: (triggerCharacter: string, data: SuggestionMenuData) => void, - updatePosition: ( - triggerCharacter: string, - position: UiElementPosition - ) => void + emitUpdate: (menuName: string, state: SuggestionMenuState) => void ) { this.pluginState = undefined; - this.updateData = (triggerCharacter: string) => { - if (!this.data) { - throw new Error("Attempting to update uninitialized suggestions menu"); - } - - updateData(triggerCharacter, this.data); - }; - - this.updatePosition = (triggerCharacter: string) => { - if (!this.position) { + this.emitUpdate = (menuName: string) => { + if (!this.state) { throw new Error("Attempting to update uninitialized suggestions menu"); } - updatePosition(triggerCharacter, this.position); + emitUpdate(menuName, this.state); }; document.addEventListener("scroll", this.handleScroll); } handleScroll = () => { - if (this.position?.show) { + if (this.state?.show) { const decorationNode = document.querySelector( `[data-decoration-id="${this.pluginState!.decorationId}"]` ); - this.position.referencePos = decorationNode!.getBoundingClientRect(); - this.updatePosition(this.pluginState!.triggerCharacter!); + this.state.referencePos = decorationNode!.getBoundingClientRect(); + this.emitUpdate(this.pluginState!.triggerCharacter!); } }; @@ -83,8 +70,8 @@ class SuggestionMenuView< this.pluginState = stopped ? prev : next; if (stopped || !this.editor.isEditable) { - this.position!.show = false; - this.updatePosition(this.pluginState!.triggerCharacter); + this.state!.show = false; + this.emitUpdate(this.pluginState!.triggerCharacter); return; } @@ -94,20 +81,13 @@ class SuggestionMenuView< ); if (this.editor.isEditable) { - this.data = { + this.state = { + show: true, + referencePos: decorationNode!.getBoundingClientRect(), query: this.pluginState!.query, }; - this.updateData(this.pluginState!.triggerCharacter!); - - if (started) { - this.position = { - show: true, - referencePos: decorationNode!.getBoundingClientRect(), - }; - - this.updatePosition(this.pluginState!.triggerCharacter); - } + this.emitUpdate(this.pluginState!.triggerCharacter!); } } @@ -185,14 +165,8 @@ export class SuggestionMenuProseMirrorPlugin< view: () => { this.view = new SuggestionMenuView( editor, - (triggerCharacter, suggestionMenuData) => { - this.emit(`update data ${triggerCharacter}`, suggestionMenuData); - }, - (triggerCharacter, suggestionMenuPosition) => { - this.emit( - `update position ${triggerCharacter}`, - suggestionMenuPosition - ); + (triggerCharacter, state) => { + this.emit(`update ${triggerCharacter}`, state); } ); return this.view; @@ -343,25 +317,15 @@ export class SuggestionMenuProseMirrorPlugin< }); } - public onDataUpdate( + public onUpdate( triggerCharacter: string, - callback: (data: SuggestionMenuData) => void + callback: (state: SuggestionMenuState) => void ) { if (!this.triggerCharacters.includes(triggerCharacter)) { this.addTriggerCharacter(triggerCharacter); } // TODO: be able to remove the triggerCharacter - return this.on(`update data ${triggerCharacter}`, callback); - } - - public onPositionUpdate( - triggerCharacter: string, - callback: (position: UiElementPosition) => void - ) { - if (!this.triggerCharacters.includes(triggerCharacter)) { - this.addTriggerCharacter(triggerCharacter); - } - return this.on(`update position ${triggerCharacter}`, callback); + return this.on(`update ${triggerCharacter}`, callback); } addTriggerCharacter = (triggerCharacter: string) => { diff --git a/packages/react/src/components-shared/DefaultPositionedUiElement.tsx b/packages/react/src/components-shared/DefaultPositionedUiElement.tsx new file mode 100644 index 0000000000..93edde953d --- /dev/null +++ b/packages/react/src/components-shared/DefaultPositionedUiElement.tsx @@ -0,0 +1,59 @@ +// import { +// BlockNoteEditor, +// BlockSchema, +// DefaultBlockSchema, +// DefaultInlineContentSchema, +// DefaultStyleSchema, +// InlineContentSchema, +// StyleSchema, +// UiElementPosition, +// } from "@blocknote/core"; +// import { FC } from "react"; +// +// import { DefaultImageToolbar } from "./DefaultImageToolbar"; +// import { useUiElement } from "../../hooks/useUiElement"; +// import { useUiElementPosition } from "../../hooks/useUiElementPosition"; +// import { flip, offset, UseFloatingOptions } from "@floating-ui/react"; +// +// export const DefaultPositionedUiElement = < +// State, +// BSchema extends BlockSchema = DefaultBlockSchema, +// I extends InlineContentSchema = DefaultInlineContentSchema, +// S extends StyleSchema = DefaultStyleSchema +// >(props: { +// editor: BlockNoteEditor; +// onUpdate: (callback: (state: State) => void) => void; +// zIndex: number; +// options?: Partial; +// component?: FC< +// { editor: BlockNoteEditor } & Omit< +// State, +// keyof UiElementPosition +// > +// >; +// }) => { +// const state = useUiElement(props.onUpdate); +// const { isMounted, ref, style } = useUiElementPosition( +// state?.show || false, +// state?.referencePos || null, +// 5000, +// { +// placement: "bottom", +// middleware: [offset(10), flip()], +// } +// ); +// +// if (!isMounted || !state) { +// return null; +// } +// +// const { show, referencePos, ...rest } = state; +// +// const ImageToolbar = props.imageToolbar || DefaultImageToolbar; +// +// return ( +//
+// +//
+// ); +// }; diff --git a/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx b/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx index 39468400e2..60b0ebf264 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx @@ -16,10 +16,15 @@ import { } from "./DefaultButtons/NestBlockButtons"; import { CreateLinkButton } from "./DefaultButtons/CreateLinkButton"; -export const DefaultFormattingToolbar = (props: { +export type FormattingToolbarProps = { editor: BlockNoteEditor; - blockTypeDropdownItems?: BlockTypeDropdownItem[]; -}) => { +}; + +export const DefaultFormattingToolbar = ( + props: FormattingToolbarProps & { + blockTypeDropdownItems?: BlockTypeDropdownItem[]; + } +) => { return ( diff --git a/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx b/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx index 48777c509c..de4d894a52 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx @@ -2,21 +2,82 @@ import { BlockNoteEditor, BlockSchema, DefaultBlockSchema, + DefaultProps, } from "@blocknote/core"; -import { FC } from "react"; +import { flip, offset } from "@floating-ui/react"; +import { FC, useState } from "react"; -import { useFormattingToolbarPosition } from "./hooks/useFormattingToolbarPosition"; -import { DefaultFormattingToolbar } from "./DefaultFormattingToolbar"; +import { useUiElement } from "../../hooks/useUiElement"; +import { useUiElementPosition } from "../../hooks/useUiElementPosition"; +import { useEditorChange } from "../../hooks/useEditorChange"; +import { + DefaultFormattingToolbar, + FormattingToolbarProps, +} from "./DefaultFormattingToolbar"; + +const textAlignmentToPlacement = ( + textAlignment: DefaultProps["textAlignment"] +) => { + switch (textAlignment) { + case "left": + return "top-start"; + case "center": + return "top"; + case "right": + return "top-end"; + default: + return "top-start"; + } +}; export const DefaultPositionedFormattingToolbar = < BSchema extends BlockSchema = DefaultBlockSchema >(props: { editor: BlockNoteEditor; - formattingToolbar?: FC<{ editor: BlockNoteEditor }>; + formattingToolbar?: FC>; }) => { - const { isMounted, ref, style } = useFormattingToolbarPosition(props.editor); + const [placement, setPlacement] = useState<"top-start" | "top" | "top-end">( + () => { + const block = props.editor.getTextCursorPosition().block; + + if (!("textAlignment" in block.props)) { + return "top-start"; + } + + return textAlignmentToPlacement( + block.props.textAlignment as DefaultProps["textAlignment"] + ); + } + ); + + useEditorChange(props.editor, () => { + const block = props.editor.getTextCursorPosition().block; + + if (!("textAlignment" in block.props)) { + setPlacement("top-start"); + } else { + setPlacement( + textAlignmentToPlacement( + block.props.textAlignment as DefaultProps["textAlignment"] + ) + ); + } + }); + + const state = useUiElement( + props.editor.formattingToolbar.onUpdate.bind(props.editor.formattingToolbar) + ); + const { isMounted, ref, style } = useUiElementPosition( + state?.show || false, + state?.referencePos || null, + 3000, + { + placement, + middleware: [offset(10), flip()], + } + ); - if (!isMounted) { + if (!isMounted || !state) { return null; } diff --git a/packages/react/src/components/FormattingToolbar/hooks/useFormattingToolbarPosition.ts b/packages/react/src/components/FormattingToolbar/hooks/useFormattingToolbarPosition.ts deleted file mode 100644 index b6c9fbd17f..0000000000 --- a/packages/react/src/components/FormattingToolbar/hooks/useFormattingToolbarPosition.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultProps, - DefaultStyleSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { - flip, - offset, - useFloating, - useTransitionStyles, -} from "@floating-ui/react"; -import { useEffect, useRef, useState } from "react"; -import { useEditorChange } from "../../../hooks/useEditorChange"; -import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; - -const textAlignmentToPlacement = ( - textAlignment: DefaultProps["textAlignment"] -) => { - switch (textAlignment) { - case "left": - return "top-start"; - case "center": - return "top"; - case "right": - return "top-end"; - default: - return "top-start"; - } -}; - -export function useFormattingToolbarPosition< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->(editor: BlockNoteEditor): UiComponentPosition { - const [show, setShow] = useState(false); - const referencePos = useRef(); - const [placement, setPlacement] = useState<"top-start" | "top" | "top-end">( - () => { - const block = editor.getTextCursorPosition().block; - - if (!("textAlignment" in block.props)) { - return "top-start"; - } - - return textAlignmentToPlacement( - block.props.textAlignment as DefaultProps["textAlignment"] - ); - } - ); - - const { refs, update, context, floatingStyles } = useFloating({ - open: show, - placement, - middleware: [offset(10), flip()], - }); - - const { isMounted, styles } = useTransitionStyles(context); - - useEffect(() => { - return editor.formattingToolbar.onPositionUpdate((position) => { - setShow(position.show); - - referencePos.current = position.referencePos; - - update(); - }); - }, [editor, update]); - - useEditorChange(editor, () => { - const block = editor.getTextCursorPosition().block; - - if (!("textAlignment" in block.props)) { - setPlacement("top-start"); - } else { - setPlacement( - textAlignmentToPlacement( - block.props.textAlignment as DefaultProps["textAlignment"] - ) - ); - } - }); - - useEffect(() => { - refs.setReference({ - getBoundingClientRect: () => referencePos.current!, - }); - }, [refs]); - - return { - isMounted: isMounted, - ref: refs.setFloating, - style: { - display: "flex", - ...styles, - ...floatingStyles, - zIndex: 3000, - }, - }; -} diff --git a/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx b/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx index a275147e74..1bbbc2334c 100644 --- a/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx +++ b/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx @@ -1,24 +1,25 @@ import { BlockNoteEditor, - BlockSchema, - InlineContentSchema, - StyleSchema, + HyperlinkToolbarState, + UiElementPosition, } from "@blocknote/core"; import { useRef, useState } from "react"; import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; -import { useHyperlinkToolbarData } from "./hooks/useHyperlinkToolbarData"; import { Toolbar } from "../../components-shared/Toolbar/Toolbar"; import { ToolbarButton } from "../../components-shared/Toolbar/ToolbarButton"; import { EditHyperlinkMenu } from "./EditHyperlinkMenu/components/EditHyperlinkMenu"; -export const DefaultHyperlinkToolbar = < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->(props: { - editor: BlockNoteEditor; -}) => { +export type HyperlinkToolbarProps = Omit< + HyperlinkToolbarState, + keyof UiElementPosition +> & + Pick< + BlockNoteEditor["hyperlinkToolbar"], + "deleteHyperlink" | "editHyperlink" | "startHideTimer" | "stopHideTimer" + >; + +export const DefaultHyperlinkToolbar = (props: HyperlinkToolbarProps) => { const [isEditing, setIsEditing] = useState(false); const editMenuRef = useRef(null); @@ -29,7 +30,7 @@ export const DefaultHyperlinkToolbar = < editHyperlink, startHideTimer, stopHideTimer, - } = useHyperlinkToolbarData(props.editor); + } = props; if (isEditing) { return ( diff --git a/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx b/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx index 5e0f9e77d8..36058f5563 100644 --- a/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx +++ b/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx @@ -7,10 +7,15 @@ import { InlineContentSchema, StyleSchema, } from "@blocknote/core"; +import { flip, offset } from "@floating-ui/react"; import { FC } from "react"; -import { useHyperlinkToolbarPosition } from "./hooks/useHyperlinkToolbarPosition"; -import { DefaultHyperlinkToolbar } from "./DefaultHyperlinkToolbar"; +import { useUiElement } from "../../hooks/useUiElement"; +import { useUiElementPosition } from "../../hooks/useUiElementPosition"; +import { + DefaultHyperlinkToolbar, + HyperlinkToolbarProps, +} from "./DefaultHyperlinkToolbar"; export const DefaultPositionedHyperlinkToolbar = < BSchema extends BlockSchema = DefaultBlockSchema, @@ -18,19 +23,39 @@ export const DefaultPositionedHyperlinkToolbar = < S extends StyleSchema = DefaultStyleSchema >(props: { editor: BlockNoteEditor; - hyperlinkToolbar?: FC<{ editor: BlockNoteEditor }>; + hyperlinkToolbar?: FC; }) => { - const { isMounted, ref, style } = useHyperlinkToolbarPosition(props.editor); + const callbacks = { + deleteHyperlink: props.editor.hyperlinkToolbar.deleteHyperlink, + editHyperlink: props.editor.hyperlinkToolbar.editHyperlink, + startHideTimer: props.editor.hyperlinkToolbar.startHideTimer, + stopHideTimer: props.editor.hyperlinkToolbar.stopHideTimer, + }; - if (!isMounted) { + const state = useUiElement( + props.editor.hyperlinkToolbar.onUpdate.bind(props.editor.hyperlinkToolbar) + ); + const { isMounted, ref, style } = useUiElementPosition( + state?.show || false, + state?.referencePos || null, + 4000, + { + placement: "top-start", + middleware: [offset(10), flip()], + } + ); + + if (!isMounted || !state) { return null; } + const { show, referencePos, ...data } = state; + const HyperlinkToolbar = props.hyperlinkToolbar || DefaultHyperlinkToolbar; return (
- +
); }; diff --git a/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarData.ts b/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarData.ts deleted file mode 100644 index be8fb7a045..0000000000 --- a/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarData.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - HyperlinkToolbarData, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { useEffect, useState } from "react"; -import { UiComponentData } from "../../../components-shared/UiComponentTypes"; - -export function useHyperlinkToolbarData< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->( - editor: BlockNoteEditor -): UiComponentData { - const [text, setText] = useState(); - const [url, setUrl] = useState(); - - useEffect(() => { - return editor.hyperlinkToolbar.onDataUpdate((data) => { - setText(data.text); - setUrl(data.url); - }); - }, [editor]); - - return { - text: text!, - url: url!, - deleteHyperlink: editor.hyperlinkToolbar.deleteHyperlink, - editHyperlink: editor.hyperlinkToolbar.editHyperlink, - startHideTimer: editor.hyperlinkToolbar.startHideTimer, - stopHideTimer: editor.hyperlinkToolbar.stopHideTimer, - }; -} diff --git a/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarPosition.ts b/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarPosition.ts deleted file mode 100644 index c59554fabb..0000000000 --- a/packages/react/src/components/HyperlinkToolbar/hooks/useHyperlinkToolbarPosition.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { - flip, - offset, - useFloating, - useTransitionStyles, -} from "@floating-ui/react"; -import { useEffect, useRef, useState } from "react"; -import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; - -export function useHyperlinkToolbarPosition< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->(editor: BlockNoteEditor): UiComponentPosition { - const [show, setShow] = useState(false); - const referencePos = useRef(); - - const { refs, update, context, floatingStyles } = useFloating({ - open: show, - placement: "top-start", - middleware: [offset(10), flip()], - }); - - const { isMounted, styles } = useTransitionStyles(context); - - useEffect(() => { - return editor.hyperlinkToolbar.onPositionUpdate((position) => { - setShow(position.show); - referencePos.current = position.referencePos; - update(); - }); - }, [editor.hyperlinkToolbar, update]); - - useEffect(() => { - refs.setReference({ - getBoundingClientRect: () => referencePos.current!, - }); - }, [refs]); - - return { - isMounted: isMounted, - ref: refs.setFloating, - style: { - display: "flex", - ...styles, - ...floatingStyles, - zIndex: 4000, - }, - }; -} diff --git a/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx b/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx index 32a08ec27d..1760ffcea8 100644 --- a/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx +++ b/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx @@ -1,4 +1,10 @@ -import { BlockNoteEditor, BlockSchema, PartialBlock } from "@blocknote/core"; +import { + BlockNoteEditor, + BlockSchema, + ImageToolbarState, + PartialBlock, + UiElementPosition, +} from "@blocknote/core"; import { Button, FileInput, @@ -15,20 +21,21 @@ import { useState, } from "react"; -import { useImageToolbarData } from "./hooks/useImageToolbarData"; import { Toolbar } from "../../components-shared/Toolbar/Toolbar"; -export const DefaultImageToolbar = (props: { +export type ImageToolbarProps = { editor: BlockNoteEditor; -}) => { +} & Omit, keyof UiElementPosition>; + +export const DefaultImageToolbar = ( + props: ImageToolbarProps +) => { const [openTab, setOpenTab] = useState<"upload" | "embed">( props.editor.uploadFile !== undefined ? "upload" : "embed" ); const [uploading, setUploading] = useState(false); const [uploadFailed, setUploadFailed] = useState(false); - const { block } = useImageToolbarData(props.editor); - useEffect(() => { if (uploadFailed) { setTimeout(() => { @@ -49,7 +56,7 @@ export const DefaultImageToolbar = (props: { if (props.editor.uploadFile !== undefined) { try { const uploaded = await props.editor.uploadFile(file); - props.editor.updateBlock(block, { + props.editor.updateBlock(props.block, { type: "image", props: { url: uploaded, @@ -65,7 +72,7 @@ export const DefaultImageToolbar = (props: { upload(file); }, - [block, props.editor] + [props.block, props.editor] ); const [currentURL, setCurrentURL] = useState(""); @@ -81,7 +88,7 @@ export const DefaultImageToolbar = (props: { (event: KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault(); - props.editor.updateBlock(block, { + props.editor.updateBlock(props.block, { type: "image", props: { url: currentURL, @@ -89,17 +96,17 @@ export const DefaultImageToolbar = (props: { } as PartialBlock); } }, - [currentURL, block, props.editor] + [currentURL, props.block, props.editor] ); const handleURLClick = useCallback(() => { - props.editor.updateBlock(block, { + props.editor.updateBlock(props.block, { type: "image", props: { url: currentURL, }, } as PartialBlock); - }, [currentURL, block, props.editor]); + }, [currentURL, props.block, props.editor]); return ( diff --git a/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx b/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx index 9224494920..bada0a6dff 100644 --- a/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx +++ b/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx @@ -2,32 +2,44 @@ import { BlockNoteEditor, BlockSchema, DefaultBlockSchema, - DefaultInlineContentSchema, - InlineContentSchema, } from "@blocknote/core"; import { FC } from "react"; +import { flip, offset } from "@floating-ui/react"; -import { DefaultImageToolbar } from "./DefaultImageToolbar"; -import { useImageToolbarPosition } from "./hooks/useImageToolbarPosition"; +import { useUiElement } from "../../hooks/useUiElement"; +import { useUiElementPosition } from "../../hooks/useUiElementPosition"; +import { DefaultImageToolbar, ImageToolbarProps } from "./DefaultImageToolbar"; export const DefaultPositionedImageToolbar = < - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema + BSchema extends BlockSchema = DefaultBlockSchema >(props: { - editor: BlockNoteEditor; - imageToolbar?: FC<{ editor: BlockNoteEditor }>; + editor: BlockNoteEditor; + imageToolbar?: FC>; }) => { - const { isMounted, ref, style } = useImageToolbarPosition(props.editor); + const state = useUiElement( + props.editor.imageToolbar.onUpdate.bind(props.editor.imageToolbar) + ); + const { isMounted, ref, style } = useUiElementPosition( + state?.show || false, + state?.referencePos || null, + 5000, + { + placement: "bottom", + middleware: [offset(10), flip()], + } + ); - if (!isMounted) { + if (!isMounted || !state) { return null; } + const { show, referencePos, ...data } = state; + const ImageToolbar = props.imageToolbar || DefaultImageToolbar; return (
- +
); }; diff --git a/packages/react/src/components/ImageToolbar/hooks/useImageToolbarData.ts b/packages/react/src/components/ImageToolbar/hooks/useImageToolbarData.ts deleted file mode 100644 index bdb7e507c3..0000000000 --- a/packages/react/src/components/ImageToolbar/hooks/useImageToolbarData.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - ImageToolbarData, - InlineContentSchema, - SpecificBlock, - StyleSchema, -} from "@blocknote/core"; -import { useEffect, useState } from "react"; -import { UiComponentData } from "../../../components-shared/UiComponentTypes"; - -export function useImageToolbarData< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->( - editor: BlockNoteEditor -): UiComponentData, "imageToolbar"> { - const [block, setBlock] = useState>(); - - useEffect(() => { - return editor.imageToolbar.onDataUpdate((data) => { - setBlock(data.block); - }); - }, [editor]); - - return { - block: block!, - }; -} diff --git a/packages/react/src/components/ImageToolbar/hooks/useImageToolbarPosition.ts b/packages/react/src/components/ImageToolbar/hooks/useImageToolbarPosition.ts deleted file mode 100644 index 46174dca5e..0000000000 --- a/packages/react/src/components/ImageToolbar/hooks/useImageToolbarPosition.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { - flip, - offset, - useFloating, - useTransitionStyles, -} from "@floating-ui/react"; -import { useEffect, useRef, useState } from "react"; -import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; - -export function useImageToolbarPosition< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->(editor: BlockNoteEditor): UiComponentPosition { - const [show, setShow] = useState(false); - const referencePos = useRef(); - - const { refs, update, context, floatingStyles } = useFloating({ - open: show, - placement: "bottom", - middleware: [offset(10), flip()], - }); - - const { isMounted, styles } = useTransitionStyles(context); - - useEffect(() => { - return editor.imageToolbar.onPositionUpdate((position) => { - setShow(position.show); - referencePos.current = position.referencePos; - update(); - }); - }, [editor.imageToolbar, update]); - - useEffect(() => { - refs.setReference({ - getBoundingClientRect: () => referencePos.current!, - }); - }, [refs]); - - return { - isMounted: isMounted, - ref: refs.setFloating, - style: { - display: "flex", - ...styles, - ...floatingStyles, - zIndex: 5000, - }, - }; -} diff --git a/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx index 6ea13d5fa4..5ad77ca53e 100644 --- a/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx +++ b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx @@ -9,8 +9,9 @@ import { } from "@blocknote/core"; import { FC } from "react"; +import { useUiElement } from "../../hooks/useUiElement"; +import { useUiElementPosition } from "../../hooks/useUiElementPosition"; import { DefaultSideMenu } from "./DefaultSideMenu"; -import { useSideMenuPosition } from "./hooks/useSideMenuPosition"; export const DefaultPositionedSideMenu = < BSchema extends BlockSchema = DefaultBlockSchema, @@ -22,17 +23,37 @@ export const DefaultPositionedSideMenu = < editor: BlockNoteEditor; }>; }) => { - const { isMounted, ref, style } = useSideMenuPosition(props.editor); + const callbacks = { + addBlock: props.editor.sideMenu.addBlock, + blockDragStart: props.editor.sideMenu.blockDragStart, + blockDragEnd: props.editor.sideMenu.blockDragEnd, + freezeMenu: props.editor.sideMenu.freezeMenu, + unfreezeMenu: props.editor.sideMenu.unfreezeMenu, + }; - if (!isMounted) { + const state = useUiElement( + props.editor.sideMenu.onUpdate.bind(props.editor.sideMenu) + ); + const { isMounted, ref, style } = useUiElementPosition( + state?.show || false, + state?.referencePos || null, + 1000, + { + placement: "left", + } + ); + + if (!isMounted || !state) { return null; } + const { show, referencePos, ...data } = state; + const SideMenu = props.sideMenu || DefaultSideMenu; return (
- +
); }; diff --git a/packages/react/src/components/SideMenu/DefaultSideMenu.tsx b/packages/react/src/components/SideMenu/DefaultSideMenu.tsx index 00a7c11d7f..31cb3fd43c 100644 --- a/packages/react/src/components/SideMenu/DefaultSideMenu.tsx +++ b/packages/react/src/components/SideMenu/DefaultSideMenu.tsx @@ -2,45 +2,46 @@ import { BlockNoteEditor, BlockSchema, InlineContentSchema, + SideMenuState, + StyleSchema, + UiElementPosition, } from "@blocknote/core"; - -import { StyleSchema } from "@blocknote/core"; import { AddBlockButton } from "./DefaultButtons/AddBlockButton"; import { DragHandle } from "./DefaultButtons/DragHandle"; import { SideMenu } from "./SideMenu"; -import { useSideMenuData } from "./hooks/useSideMenuData"; import { FC } from "react"; import type { DragHandleMenuProps } from "./DragHandleMenu/DragHandleMenu"; -export const DefaultSideMenu = < +export type SideMenuProps< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->(props: { +> = { editor: BlockNoteEditor; dragHandleMenu?: FC>; -}) => { - const { - block, - addBlock, - blockDragStart, - blockDragEnd, - freezeMenu, - unfreezeMenu, - } = useSideMenuData(props.editor); +} & Omit, keyof UiElementPosition> & + Pick< + BlockNoteEditor["sideMenu"], + | "addBlock" + | "blockDragStart" + | "blockDragEnd" + | "freezeMenu" + | "unfreezeMenu" + >; + +export const DefaultSideMenu = < + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + props: SideMenuProps +) => { + const { addBlock, ...rest } = props; return ( - + ); }; diff --git a/packages/react/src/components/SideMenu/hooks/useSideMenuData.ts b/packages/react/src/components/SideMenu/hooks/useSideMenuData.ts deleted file mode 100644 index 58c68c13bc..0000000000 --- a/packages/react/src/components/SideMenu/hooks/useSideMenuData.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - Block, - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - InlineContentSchema, - SideMenuData, - StyleSchema, -} from "@blocknote/core"; -import { useEffect, useState } from "react"; -import { UiComponentData } from "../../../components-shared/UiComponentTypes"; - -export function useSideMenuData< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->( - editor: BlockNoteEditor -): UiComponentData, "sideMenu"> { - const [block, setBlock] = useState>(); - - useEffect(() => { - return editor.sideMenu.onDataUpdate((data) => { - setBlock(data.block); - }); - }, [editor]); - - return { - block: block!, - addBlock: editor.sideMenu.addBlock, - blockDragStart: editor.sideMenu.blockDragStart, - blockDragEnd: editor.sideMenu.blockDragEnd, - freezeMenu: editor.sideMenu.freezeMenu, - unfreezeMenu: editor.sideMenu.unfreezeMenu, - }; -} diff --git a/packages/react/src/components/SideMenu/hooks/useSideMenuPosition.ts b/packages/react/src/components/SideMenu/hooks/useSideMenuPosition.ts deleted file mode 100644 index 39526ed74c..0000000000 --- a/packages/react/src/components/SideMenu/hooks/useSideMenuPosition.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { useFloating, useTransitionStyles } from "@floating-ui/react"; -import { useEffect, useRef, useState } from "react"; -import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; - -export function useSideMenuPosition< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->(editor: BlockNoteEditor): UiComponentPosition { - const [show, setShow] = useState(false); - const referencePos = useRef(); - - const { refs, update, context, floatingStyles } = useFloating({ - open: show, - placement: "left", - }); - - const { isMounted, styles } = useTransitionStyles(context); - - useEffect(() => { - return editor.sideMenu.onPositionUpdate((position) => { - setShow(position.show); - referencePos.current = position.referencePos; - update(); - }); - }, [editor.sideMenu, update]); - - useEffect(() => { - refs.setReference({ - getBoundingClientRect: () => referencePos.current!, - }); - }, [refs]); - - return { - isMounted: isMounted, - ref: refs.setFloating, - style: { - display: "flex", - ...styles, - ...floatingStyles, - zIndex: 1000, - }, - }; -} diff --git a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx index 58954d714e..2dc500e4d7 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx @@ -1,12 +1,15 @@ -import { FC } from "react"; import { BlockNoteEditor, BlockSchema, InlineContentSchema, StyleSchema, + SuggestionMenuState, } from "@blocknote/core"; +import { flip, offset, size } from "@floating-ui/react"; +import { FC } from "react"; -import { useSuggestionMenuPosition } from "./hooks/useSuggestionMenuPosition"; +import { useUiElement } from "../../hooks/useUiElement"; +import { useUiElementPosition } from "../../hooks/useUiElementPosition"; import { DefaultSuggestionMenu } from "./DefaultSuggestionMenu"; import { SuggestionMenuComponentProps } from "./MantineDefaults/MantineSuggestionMenu"; import { SuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; @@ -29,23 +32,53 @@ export function DefaultPositionedSuggestionMenu< ) => Promise; suggestionMenuComponent?: FC>; }) { - const { editor, triggerCharacter, getItems, suggestionMenuComponent } = props; + const callbacks = { + closeMenu: props.editor.suggestionMenus.closeMenu, + clearQuery: props.editor.suggestionMenus.clearQuery, + }; - const { isMounted, ref, style } = useSuggestionMenuPosition( - editor, - triggerCharacter || "/" + const state = useUiElement((callback: (state: SuggestionMenuState) => void) => + props.editor.suggestionMenus.onUpdate.bind(props.editor.suggestionMenus)( + props.triggerCharacter || "/", + callback + ) + ); + const { isMounted, ref, style } = useUiElementPosition( + state?.show || false, + state?.referencePos || null, + 2000, + { + placement: "bottom-start", + middleware: [ + offset(10), + // Flips the menu placement to maximize the space available, and prevents + // the menu from being cut off by the confines of the screen. + flip(), + size({ + apply({ availableHeight, elements }) { + Object.assign(elements.floating.style, { + maxHeight: `${availableHeight - 10}px`, + }); + }, + }), + ], + } ); - if (!isMounted) { + if (!isMounted || !state) { return null; } + const { show, referencePos, ...data } = state; + return (
); diff --git a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx index 870aa2869e..1b9ff095eb 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx @@ -4,9 +4,9 @@ import { BlockSchema, InlineContentSchema, StyleSchema, + SuggestionMenuState, + UiElementPosition, } from "@blocknote/core"; - -import { useSuggestionMenuData } from "./hooks/useSuggestionMenuData"; import { useLoadSuggestionMenuItems } from "./hooks/useLoadSuggestionMenuItems"; import { useCloseSuggestionMenuNoItems } from "./hooks/useCloseSuggestionMenuNoItems"; import { useSuggestionMenuKeyboardNavigation } from "./hooks/useSuggestionMenuKeyboardNavigation"; @@ -21,20 +21,20 @@ export type SuggestionMenuProps< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema, - Item extends { - name: string; - execute: () => void; - } = SuggestionMenuItemProps + Item > = { editor: BlockNoteEditor; - triggerCharacter?: string; getItems?: ( query: string, closeMenu: () => void, clearQuery: () => void ) => Promise; suggestionMenuComponent?: FC>; -}; +} & Omit & + Pick< + BlockNoteEditor["suggestionMenus"], + "closeMenu" | "clearQuery" + >; export function DefaultSuggestionMenu< BSchema extends BlockSchema, @@ -45,12 +45,14 @@ export function DefaultSuggestionMenu< execute: () => void; } = SuggestionMenuItemProps >(props: SuggestionMenuProps) { - const { editor, triggerCharacter, getItems, suggestionMenuComponent } = props; - - const { query, closeMenu, clearQuery } = useSuggestionMenuData( + const { editor, - triggerCharacter || "/" - ); + getItems, + suggestionMenuComponent, + query, + closeMenu, + clearQuery, + } = props; const getItemsForLoading = useMemo<(query: string) => Promise>( () => (query: string) => diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuData.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuData.ts deleted file mode 100644 index 64d3670474..0000000000 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuData.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { useEffect, useState } from "react"; - -export function useSuggestionMenuData< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->(editor: BlockNoteEditor, triggerCharacter: string) { - const [query, setQuery] = useState(""); - - useEffect(() => { - return editor.suggestionMenus.onDataUpdate(triggerCharacter, (data) => { - setQuery(data.query); - }); - }, [editor.suggestionMenus, triggerCharacter]); - - return { - query: query, - closeMenu: editor.suggestionMenus.closeMenu, - clearQuery: editor.suggestionMenus.clearQuery, - }; -} diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuPosition.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuPosition.ts deleted file mode 100644 index 6cd9752525..0000000000 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuPosition.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { - flip, - offset, - size, - useFloating, - useTransitionStyles, -} from "@floating-ui/react"; -import { useEffect, useRef, useState } from "react"; - -export function useSuggestionMenuPosition< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->(editor: BlockNoteEditor, triggerCharacter: string) { - const [show, setShow] = useState(false); - - const referencePos = useRef(); - - const { refs, update, context, floatingStyles } = useFloating({ - open: show, - placement: "bottom-start", - middleware: [ - offset(10), - // Flips the menu placement to maximize the space available, and prevents - // the menu from being cut off by the confines of the screen. - flip(), - size({ - apply({ availableHeight, elements }) { - Object.assign(elements.floating.style, { - maxHeight: `${availableHeight - 10}px`, - }); - }, - }), - ], - }); - - const { isMounted, styles } = useTransitionStyles(context); - - useEffect(() => { - return editor.suggestionMenus.onPositionUpdate( - triggerCharacter, - (position) => { - setShow(position.show); - referencePos.current = position.referencePos; - update(); - } - ); - }, [editor.suggestionMenus, triggerCharacter, show, update]); - - useEffect(() => { - refs.setReference({ - getBoundingClientRect: () => referencePos.current!, - }); - }, [refs]); - - return { - isMounted: isMounted, - ref: refs.setFloating, - style: { - display: "flex", - ...styles, - ...floatingStyles, - zIndex: 2000, - }, - }; -} diff --git a/packages/react/src/hooks/useUiElement.ts b/packages/react/src/hooks/useUiElement.ts new file mode 100644 index 0000000000..35025b0a1a --- /dev/null +++ b/packages/react/src/hooks/useUiElement.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; + +export function useUiElement( + onUpdate: (callback: (state: State) => void) => void +): State | undefined { + const [state, setState] = useState(); + + useEffect(() => { + return onUpdate((state) => { + setState({ ...state }); + }); + }, [onUpdate]); + + return state; +} diff --git a/packages/react/src/hooks/useUiElementPosition.ts b/packages/react/src/hooks/useUiElementPosition.ts new file mode 100644 index 0000000000..d7d6922787 --- /dev/null +++ b/packages/react/src/hooks/useUiElementPosition.ts @@ -0,0 +1,45 @@ +import { + useFloating, + UseFloatingOptions, + useTransitionStyles, +} from "@floating-ui/react"; +import { useEffect } from "react"; +import { UiComponentPosition } from "../components-shared/UiComponentTypes"; + +export function useUiElementPosition( + show: boolean, + referencePos: DOMRect | null, + zIndex: number, + options?: Partial +): UiComponentPosition { + const { refs, update, context, floatingStyles } = useFloating({ + open: show, + ...options, + }); + + const { isMounted, styles } = useTransitionStyles(context); + + useEffect(() => { + update(); + }, [referencePos, update]); + + useEffect(() => { + // TODO: Maybe throw error instead if null + if (referencePos !== null) { + refs.setReference({ + getBoundingClientRect: () => referencePos, + }); + } + }, [referencePos, refs]); + + return { + isMounted: isMounted, + ref: refs.setFloating, + style: { + display: "flex", + ...styles, + ...floatingStyles, + zIndex: zIndex, + }, + }; +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index c9482a341f..7510a00536 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -4,7 +4,6 @@ export * from "./editor/BlockNoteTheme"; export * from "./editor/BlockNoteView"; export * from "./editor/defaultThemes"; -export * from "./components/FormattingToolbar/hooks/useFormattingToolbarPosition"; export * from "./components/FormattingToolbar/DefaultButtons/ColorStyleButton"; export * from "./components/FormattingToolbar/DefaultButtons/CreateLinkButton"; export * from "./components/FormattingToolbar/DefaultButtons/ImageCaptionButton"; @@ -16,13 +15,9 @@ export * from "./components/FormattingToolbar/DefaultDropdowns/BlockTypeDropdown export * from "./components/FormattingToolbar/DefaultFormattingToolbar"; export * from "./components/FormattingToolbar/DefaultPositionedFormattingToolbar"; -export * from "./components/HyperlinkToolbar/hooks/useHyperlinkToolbarData"; -export * from "./components/HyperlinkToolbar/hooks/useHyperlinkToolbarPosition"; export * from "./components/HyperlinkToolbar/DefaultHyperlinkToolbar"; export * from "./components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar"; -export * from "./components/SideMenu/hooks/useSideMenuData"; -export * from "./components/SideMenu/hooks/useSideMenuPosition"; export * from "./components/SideMenu/DefaultButtons/AddBlockButton"; export * from "./components/SideMenu/DefaultButtons/DragHandle"; export * from "./components/SideMenu/SideMenu"; @@ -36,8 +31,6 @@ export * from "./components/SideMenu/DragHandleMenu/DefaultDragHandleMenu"; export * from "./components/SideMenu/DragHandleMenu/DragHandleMenu"; export * from "./components/SideMenu/DragHandleMenu/DragHandleMenuItem"; -export * from "./components/SuggestionMenu/hooks/useSuggestionMenuData"; -export * from "./components/SuggestionMenu/hooks/useSuggestionMenuPosition"; export * from "./components/SuggestionMenu/hooks/useLoadSuggestionMenuItems"; export * from "./components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems"; export * from "./components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation"; @@ -47,8 +40,6 @@ export * from "./components/SuggestionMenu/defaultGetItems"; export * from "./components/SuggestionMenu/DefaultSuggestionMenu"; export * from "./components/SuggestionMenu/DefaultPositionedSuggestionMenu"; -export * from "./components/ImageToolbar/hooks/useImageToolbarData"; -export * from "./components/ImageToolbar/hooks/useImageToolbarPosition"; export * from "./components/ImageToolbar/DefaultImageToolbar"; export * from "./components/ImageToolbar/DefaultPositionedImageToolbar"; From 7f47e5a140d5f93db89f8ecafdff71135b9b5ef6 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 14 Feb 2024 17:44:18 +0100 Subject: [PATCH 3/7] Fixed vanilla example --- .../vanilla/src/ui/addFormattingToolbar.ts | 8 ++--- .../vanilla/src/ui/addHyperlinkToolbar.ts | 16 ++++------ examples/vanilla/src/ui/addSideMenu.ts | 8 ++--- examples/vanilla/src/ui/addSlashMenu.ts | 32 +++++++------------ .../DefaultButtons/ReplaceImageButton.tsx | 2 +- 5 files changed, 28 insertions(+), 38 deletions(-) diff --git a/examples/vanilla/src/ui/addFormattingToolbar.ts b/examples/vanilla/src/ui/addFormattingToolbar.ts index 3a0a557c0e..c05fb389f4 100644 --- a/examples/vanilla/src/ui/addFormattingToolbar.ts +++ b/examples/vanilla/src/ui/addFormattingToolbar.ts @@ -21,14 +21,14 @@ export const addFormattingToolbar = (editor: BlockNoteEditor) => { document.getElementById("root")!.appendChild(element); - editor.formattingToolbar.onPositionUpdate((position) => { - if (position.show) { + editor.formattingToolbar.onUpdate((state) => { + if (state.show) { element.style.display = "block"; boldBtn.text = "bold" in editor.getActiveStyles() ? "unset bold" : "set bold"; - element.style.top = position.referencePos.top + "px"; - element.style.left = position.referencePos.x - element.offsetWidth + "px"; + element.style.top = state.referencePos.top + "px"; + element.style.left = state.referencePos.x - element.offsetWidth + "px"; } else { element.style.display = "none"; } diff --git a/examples/vanilla/src/ui/addHyperlinkToolbar.ts b/examples/vanilla/src/ui/addHyperlinkToolbar.ts index 4e09bd892b..9a9a218f91 100644 --- a/examples/vanilla/src/ui/addHyperlinkToolbar.ts +++ b/examples/vanilla/src/ui/addHyperlinkToolbar.ts @@ -25,19 +25,17 @@ export const addHyperlinkToolbar = (editor: BlockNoteEditor) => { document.getElementById("root")!.appendChild(element); - editor.hyperlinkToolbar.onPositionUpdate((position) => { - if (position.show) { + editor.hyperlinkToolbar.onUpdate((state) => { + if (state.show) { + url = state.url; + text = state.text; + element.style.display = "block"; - element.style.top = position.referencePos.top + "px"; - element.style.left = position.referencePos.x - element.offsetWidth + "px"; + element.style.top = state.referencePos.top + "px"; + element.style.left = state.referencePos.x - element.offsetWidth + "px"; } else { element.style.display = "none"; } }); - - editor.hyperlinkToolbar.onDataUpdate((data) => { - url = data.url; - text = data.text; - }); }; diff --git a/examples/vanilla/src/ui/addSideMenu.ts b/examples/vanilla/src/ui/addSideMenu.ts index 845b7e9667..ecb9f4f91c 100644 --- a/examples/vanilla/src/ui/addSideMenu.ts +++ b/examples/vanilla/src/ui/addSideMenu.ts @@ -24,12 +24,12 @@ export const addSideMenu = (editor: BlockNoteEditor) => { document.getElementById("root")!.appendChild(element); - editor.sideMenu.onPositionUpdate((position) => { - if (position.show) { + editor.sideMenu.onUpdate((state) => { + if (state.show) { element.style.display = "block"; - element.style.top = position.referencePos.top + "px"; - element.style.left = position.referencePos.x - element.offsetWidth + "px"; + element.style.top = state.referencePos.top + "px"; + element.style.left = state.referencePos.x - element.offsetWidth + "px"; } else { element.style.display = "none"; } diff --git a/examples/vanilla/src/ui/addSlashMenu.ts b/examples/vanilla/src/ui/addSlashMenu.ts index 8142191ab0..a3f62e3bfe 100644 --- a/examples/vanilla/src/ui/addSlashMenu.ts +++ b/examples/vanilla/src/ui/addSlashMenu.ts @@ -2,7 +2,13 @@ import { BlockNoteEditor } from "@blocknote/core"; import { createButton } from "./util"; export const addSlashMenu = async (editor: BlockNoteEditor) => { - let element: HTMLElement; + const element = document.createElement("div"); + element.style.background = "gray"; + element.style.display = "none"; + element.style.opacity = "0.8"; + element.style.padding = "10px"; + element.style.position = "absolute"; + const getItems = async (query: string) => { const items = [ { @@ -88,28 +94,14 @@ export const addSlashMenu = async (editor: BlockNoteEditor) => { return domItems; } - editor.suggestionMenus.onDataUpdate("/", async (slashMenuData) => { - if (!element) { - element = document.createElement("div"); - element.style.background = "gray"; - element.style.position = "absolute"; - element.style.padding = "10px"; - element.style.opacity = "0.8"; - element.style.display = "none"; - - document.getElementById("root")!.appendChild(element); - } - - await updateItems(slashMenuData.query, getItems); - }); + editor.suggestionMenus.onUpdate("/", async (state) => { + if (state.show) { + await updateItems(state.query, getItems); - editor.suggestionMenus.onPositionUpdate("/", (slashMenuPosition) => { - if (slashMenuPosition.show) { element.style.display = "block"; - element.style.top = slashMenuPosition.referencePos.top + "px"; - element.style.left = - slashMenuPosition.referencePos.x - element.offsetWidth + "px"; + element.style.top = state.referencePos.top + "px"; + element.style.left = state.referencePos.x - element.offsetWidth + "px"; } else { element.style.display = "none"; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/ReplaceImageButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/ReplaceImageButton.tsx index 9e27b7c507..6a3df0e517 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/ReplaceImageButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/ReplaceImageButton.tsx @@ -39,7 +39,7 @@ export const ReplaceImageButton = (props: { /> - + ); From 3f29e9031e943f285f04c0ef1ec5fb72c643d3be Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 14 Feb 2024 17:49:38 +0100 Subject: [PATCH 4/7] Lint fix --- .../DefaultPositionedUiElement.tsx | 116 +++++++++--------- 1 file changed, 57 insertions(+), 59 deletions(-) diff --git a/packages/react/src/components-shared/DefaultPositionedUiElement.tsx b/packages/react/src/components-shared/DefaultPositionedUiElement.tsx index 93edde953d..0d5d4af9b3 100644 --- a/packages/react/src/components-shared/DefaultPositionedUiElement.tsx +++ b/packages/react/src/components-shared/DefaultPositionedUiElement.tsx @@ -1,59 +1,57 @@ -// import { -// BlockNoteEditor, -// BlockSchema, -// DefaultBlockSchema, -// DefaultInlineContentSchema, -// DefaultStyleSchema, -// InlineContentSchema, -// StyleSchema, -// UiElementPosition, -// } from "@blocknote/core"; -// import { FC } from "react"; -// -// import { DefaultImageToolbar } from "./DefaultImageToolbar"; -// import { useUiElement } from "../../hooks/useUiElement"; -// import { useUiElementPosition } from "../../hooks/useUiElementPosition"; -// import { flip, offset, UseFloatingOptions } from "@floating-ui/react"; -// -// export const DefaultPositionedUiElement = < -// State, -// BSchema extends BlockSchema = DefaultBlockSchema, -// I extends InlineContentSchema = DefaultInlineContentSchema, -// S extends StyleSchema = DefaultStyleSchema -// >(props: { -// editor: BlockNoteEditor; -// onUpdate: (callback: (state: State) => void) => void; -// zIndex: number; -// options?: Partial; -// component?: FC< -// { editor: BlockNoteEditor } & Omit< -// State, -// keyof UiElementPosition -// > -// >; -// }) => { -// const state = useUiElement(props.onUpdate); -// const { isMounted, ref, style } = useUiElementPosition( -// state?.show || false, -// state?.referencePos || null, -// 5000, -// { -// placement: "bottom", -// middleware: [offset(10), flip()], -// } -// ); -// -// if (!isMounted || !state) { -// return null; -// } -// -// const { show, referencePos, ...rest } = state; -// -// const ImageToolbar = props.imageToolbar || DefaultImageToolbar; -// -// return ( -//
-// -//
-// ); -// }; +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, + UiElementPosition, +} from "@blocknote/core"; +import { flip, offset, UseFloatingOptions } from "@floating-ui/react"; +import { FC } from "react"; +import { useUiElement } from "../hooks/useUiElement"; +import { useUiElementPosition } from "../hooks/useUiElementPosition"; + +export const DefaultPositionedUiElement = < + State extends UiElementPosition, + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(props: { + editor: BlockNoteEditor; + onUpdate: (callback: (state: State) => void) => void; + zIndex: number; + options?: Partial; + component: FC< + { editor: BlockNoteEditor } & Omit< + State, + keyof UiElementPosition + > + >; +}) => { + const state = useUiElement(props.onUpdate); + const { isMounted, ref, style } = useUiElementPosition( + state?.show || false, + state?.referencePos || null, + 5000, + { + placement: "bottom", + middleware: [offset(10), flip()], + } + ); + + if (!isMounted || !state) { + return null; + } + + const { show, referencePos, ...rest } = state; + + const Component = props.component; + + return ( +
+ +
+ ); +}; From c9661acae34a8c84f6e7a0d356835bb53b9d4dfb Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 14 Feb 2024 19:05:01 +0100 Subject: [PATCH 5/7] Minor fixes --- .../DefaultFormattingToolbar.tsx | 10 ++++++-- .../ImageToolbar/DefaultImageToolbar.tsx | 5 +++- .../SideMenu/DefaultPositionedSideMenu.tsx | 6 ++--- .../components/SideMenu/DefaultSideMenu.tsx | 15 ++++++----- .../DefaultPositionedSuggestionMenu.tsx | 8 +++--- .../SuggestionMenu/DefaultSuggestionMenu.tsx | 24 +++++++++++------- .../MantineDefaults/MantineSuggestionMenu.tsx | 6 ++--- .../MantineSuggestionMenuItem.tsx | 6 +++-- .../SuggestionMenu/defaultGetItems.tsx | 8 +++--- .../hooks/useCloseSuggestionMenuNoItems.ts | 4 +-- .../useSuggestionMenuKeyboardNavigation.ts | 4 +-- .../react/src/hooks/useUiElementPosition.ts | 25 +++++++++++-------- 12 files changed, 71 insertions(+), 50 deletions(-) diff --git a/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx b/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx index 60b0ebf264..794e14830d 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx @@ -1,4 +1,8 @@ -import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, +} from "@blocknote/core"; import { Toolbar } from "../../components-shared/Toolbar/Toolbar"; import { @@ -20,7 +24,9 @@ export type FormattingToolbarProps = { editor: BlockNoteEditor; }; -export const DefaultFormattingToolbar = ( +export const DefaultFormattingToolbar = < + BSchema extends BlockSchema = DefaultBlockSchema +>( props: FormattingToolbarProps & { blockTypeDropdownItems?: BlockTypeDropdownItem[]; } diff --git a/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx b/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx index 1760ffcea8..b8444e5f7c 100644 --- a/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx +++ b/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx @@ -1,6 +1,7 @@ import { BlockNoteEditor, BlockSchema, + DefaultBlockSchema, ImageToolbarState, PartialBlock, UiElementPosition, @@ -23,7 +24,9 @@ import { import { Toolbar } from "../../components-shared/Toolbar/Toolbar"; -export type ImageToolbarProps = { +export type ImageToolbarProps< + BSchema extends BlockSchema = DefaultBlockSchema +> = { editor: BlockNoteEditor; } & Omit, keyof UiElementPosition>; diff --git a/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx index 5ad77ca53e..1da0a668be 100644 --- a/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx +++ b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx @@ -11,7 +11,7 @@ import { FC } from "react"; import { useUiElement } from "../../hooks/useUiElement"; import { useUiElementPosition } from "../../hooks/useUiElementPosition"; -import { DefaultSideMenu } from "./DefaultSideMenu"; +import { DefaultSideMenu, SideMenuProps } from "./DefaultSideMenu"; export const DefaultPositionedSideMenu = < BSchema extends BlockSchema = DefaultBlockSchema, @@ -19,9 +19,7 @@ export const DefaultPositionedSideMenu = < S extends StyleSchema = DefaultStyleSchema >(props: { editor: BlockNoteEditor; - sideMenu?: FC<{ - editor: BlockNoteEditor; - }>; + sideMenu?: FC>; }) => { const callbacks = { addBlock: props.editor.sideMenu.addBlock, diff --git a/packages/react/src/components/SideMenu/DefaultSideMenu.tsx b/packages/react/src/components/SideMenu/DefaultSideMenu.tsx index 31cb3fd43c..63db4a328d 100644 --- a/packages/react/src/components/SideMenu/DefaultSideMenu.tsx +++ b/packages/react/src/components/SideMenu/DefaultSideMenu.tsx @@ -1,6 +1,9 @@ import { BlockNoteEditor, BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, InlineContentSchema, SideMenuState, StyleSchema, @@ -13,9 +16,9 @@ import { FC } from "react"; import type { DragHandleMenuProps } from "./DragHandleMenu/DragHandleMenu"; export type SideMenuProps< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema > = { editor: BlockNoteEditor; dragHandleMenu?: FC>; @@ -30,9 +33,9 @@ export type SideMenuProps< >; export const DefaultSideMenu = < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema >( props: SideMenuProps ) => { diff --git a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx index 2dc500e4d7..644736382b 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx @@ -11,8 +11,8 @@ import { FC } from "react"; import { useUiElement } from "../../hooks/useUiElement"; import { useUiElementPosition } from "../../hooks/useUiElementPosition"; import { DefaultSuggestionMenu } from "./DefaultSuggestionMenu"; -import { SuggestionMenuComponentProps } from "./MantineDefaults/MantineSuggestionMenu"; -import { SuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; +import { MantineSuggestionMenuProps } from "./MantineDefaults/MantineSuggestionMenu"; +import { MantineSuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; export function DefaultPositionedSuggestionMenu< BSchema extends BlockSchema, @@ -21,7 +21,7 @@ export function DefaultPositionedSuggestionMenu< Item extends { name: string; execute: () => void; - } = SuggestionMenuItemProps + } = MantineSuggestionMenuItemProps >(props: { editor: BlockNoteEditor; triggerCharacter?: string; @@ -30,7 +30,7 @@ export function DefaultPositionedSuggestionMenu< closeMenu: () => void, clearQuery: () => void ) => Promise; - suggestionMenuComponent?: FC>; + suggestionMenuComponent?: FC>; }) { const callbacks = { closeMenu: props.editor.suggestionMenus.closeMenu, diff --git a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx index 1b9ff095eb..5a9a372d48 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx @@ -2,6 +2,9 @@ import { FC, useMemo } from "react"; import { BlockNoteEditor, BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, InlineContentSchema, StyleSchema, SuggestionMenuState, @@ -13,15 +16,18 @@ import { useSuggestionMenuKeyboardNavigation } from "./hooks/useSuggestionMenuKe import { defaultGetItems } from "./defaultGetItems"; import { MantineSuggestionMenu, - SuggestionMenuComponentProps, + MantineSuggestionMenuProps, } from "./MantineDefaults/MantineSuggestionMenu"; -import { SuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; +import { MantineSuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; export type SuggestionMenuProps< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, - Item + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema, + Item extends { + name: string; + execute: () => void; + } = MantineSuggestionMenuItemProps > = { editor: BlockNoteEditor; getItems?: ( @@ -29,7 +35,7 @@ export type SuggestionMenuProps< closeMenu: () => void, clearQuery: () => void ) => Promise; - suggestionMenuComponent?: FC>; + suggestionMenuComponent?: FC>; } & Omit & Pick< BlockNoteEditor["suggestionMenus"], @@ -43,7 +49,7 @@ export function DefaultSuggestionMenu< Item extends { name: string; execute: () => void; - } = SuggestionMenuItemProps + } >(props: SuggestionMenuProps) { const { editor, @@ -77,7 +83,7 @@ export function DefaultSuggestionMenu< closeMenu ); - const SuggestionMenuComponent: FC> = + const SuggestionMenuComponent: FC> = suggestionMenuComponent || MantineSuggestionMenu; return ( diff --git a/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenu.tsx index 7be3e75134..d682c04849 100644 --- a/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenu.tsx @@ -2,17 +2,17 @@ import { Loader, Menu } from "@mantine/core"; import { Children, useMemo } from "react"; import { MantineSuggestionMenuItem, - SuggestionMenuItemProps, + MantineSuggestionMenuItemProps, } from "./MantineSuggestionMenuItem"; -export type SuggestionMenuComponentProps = { +export type MantineSuggestionMenuProps = { items: T[]; loadingState: "loading-initial" | "loading" | "loaded"; selectedIndex: number; }; export function MantineSuggestionMenu( - props: SuggestionMenuComponentProps + props: MantineSuggestionMenuProps ) { const { items, loadingState, selectedIndex } = props; diff --git a/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenuItem.tsx b/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenuItem.tsx index df45952187..68cc266cf2 100644 --- a/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenuItem.tsx +++ b/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenuItem.tsx @@ -3,7 +3,7 @@ import { Badge, Menu, Stack, Text } from "@mantine/core"; const MIN_LEFT_MARGIN = 5; -export type SuggestionMenuItemProps = { +export type MantineSuggestionMenuItemProps = { name: string; execute: () => void; subtext?: string; @@ -15,7 +15,9 @@ export type SuggestionMenuItemProps = { group?: string; }; -export function MantineSuggestionMenuItem(props: SuggestionMenuItemProps) { +export function MantineSuggestionMenuItem( + props: MantineSuggestionMenuItemProps +) { const itemRef = useRef(null); function isSelected() { diff --git a/packages/react/src/components/SuggestionMenu/defaultGetItems.tsx b/packages/react/src/components/SuggestionMenu/defaultGetItems.tsx index 1e1f8dfe05..3b5516a9b0 100644 --- a/packages/react/src/components/SuggestionMenu/defaultGetItems.tsx +++ b/packages/react/src/components/SuggestionMenu/defaultGetItems.tsx @@ -23,7 +23,7 @@ import { RiText, } from "react-icons/ri"; -import { SuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; +import { MantineSuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; // Sets the editor's text cursor position to the next content editable block, // so either a block with inline content or a table. The last block is always a @@ -98,8 +98,8 @@ export async function defaultGetItems< query: string, closeMenu: () => void, clearQuery: () => void -): Promise { - const items: SuggestionMenuItemProps[] = [ +): Promise { + const items: MantineSuggestionMenuItemProps[] = [ { name: "Heading 1", execute: () => { @@ -250,7 +250,7 @@ export async function defaultGetItems< "dropbox", ], group: "Media", - } satisfies SuggestionMenuItemProps, + } satisfies MantineSuggestionMenuItemProps, ]; // For testing async diff --git a/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts b/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts index 4f526f622f..fa9d0dfe00 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from "react"; -import { SuggestionMenuItemProps } from "../MantineDefaults/MantineSuggestionMenuItem"; +import { MantineSuggestionMenuItemProps } from "../MantineDefaults/MantineSuggestionMenuItem"; // Hook which closes the suggestion after a certain number of consecutive // invalid queries are made. An invalid query is one which returns no items, and @@ -8,7 +8,7 @@ export function useCloseSuggestionMenuNoItems< Item extends { name: string; execute: () => void; - } = SuggestionMenuItemProps + } = MantineSuggestionMenuItemProps >( items: Item[], usedQuery: string | undefined, diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts index 649c9eb021..669358bb2c 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts @@ -1,6 +1,6 @@ import { BlockNoteEditor } from "@blocknote/core"; import { useEffect, useState } from "react"; -import { SuggestionMenuItemProps } from "../MantineDefaults/MantineSuggestionMenuItem"; +import { MantineSuggestionMenuItemProps } from "../MantineDefaults/MantineSuggestionMenuItem"; // Hook which handles keyboard navigation of a suggestion menu. Arrow keys are // used to select a menu item, enter to execute it, and escape to close the @@ -9,7 +9,7 @@ export function useSuggestionMenuKeyboardNavigation< Item extends { name: string; execute: () => void; - } = SuggestionMenuItemProps + } = MantineSuggestionMenuItemProps >( editor: BlockNoteEditor, items: Item[], diff --git a/packages/react/src/hooks/useUiElementPosition.ts b/packages/react/src/hooks/useUiElementPosition.ts index d7d6922787..ddb7cc7e09 100644 --- a/packages/react/src/hooks/useUiElementPosition.ts +++ b/packages/react/src/hooks/useUiElementPosition.ts @@ -3,7 +3,7 @@ import { UseFloatingOptions, useTransitionStyles, } from "@floating-ui/react"; -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { UiComponentPosition } from "../components-shared/UiComponentTypes"; export function useUiElementPosition( @@ -32,14 +32,17 @@ export function useUiElementPosition( } }, [referencePos, refs]); - return { - isMounted: isMounted, - ref: refs.setFloating, - style: { - display: "flex", - ...styles, - ...floatingStyles, - zIndex: zIndex, - }, - }; + return useMemo( + () => ({ + isMounted, + ref: refs.setFloating, + style: { + display: "flex", + ...styles, + ...floatingStyles, + zIndex: zIndex, + }, + }), + [isMounted, styles, floatingStyles, zIndex, refs] + ); } From 8edebd119269960653f449ee0d6d4526275a1cb1 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 14 Feb 2024 21:30:22 +0100 Subject: [PATCH 6/7] Refactored table handles --- examples/editor/examples/basic/App.tsx | 6 +- packages/core/src/editor/BlockNoteEditor.ts | 12 +- .../TableHandles/TableHandlesPlugin.ts | 96 ++++--- .../DefaultPositionedSuggestionMenu.tsx | 9 +- .../SuggestionMenu/DefaultSuggestionMenu.tsx | 8 +- .../TableHandles/BlockSchemaWithTable.ts | 6 + .../DefaultPositionedTableHandles.tsx | 115 +++++++++ .../TableHandles/DefaultTableHandle.tsx | 14 +- .../components/TableHandles/TableHandle.tsx | 14 +- .../TableHandles/TableHandlePositioner.tsx | 216 ---------------- .../TableHandles/hooks/useTableHandlesData.ts | 55 ---- .../hooks/useTableHandlesPosition.ts | 241 +++++++++--------- .../react/src/editor/BlockNoteDefaultUI.tsx | 4 +- .../react/src/hooks/useUiElementPosition.ts | 13 +- packages/react/src/index.ts | 3 +- tests/src/utils/customblocks/Alert.tsx | 4 +- tests/src/utils/customblocks/Button.tsx | 4 +- tests/src/utils/customblocks/Embed.tsx | 4 +- tests/src/utils/customblocks/Image.tsx | 4 +- tests/src/utils/customblocks/ReactAlert.tsx | 4 +- tests/src/utils/customblocks/ReactImage.tsx | 4 +- tests/src/utils/customblocks/Separator.tsx | 4 +- 22 files changed, 361 insertions(+), 479 deletions(-) create mode 100644 packages/react/src/components/TableHandles/BlockSchemaWithTable.ts create mode 100644 packages/react/src/components/TableHandles/DefaultPositionedTableHandles.tsx delete mode 100644 packages/react/src/components/TableHandles/TableHandlePositioner.tsx delete mode 100644 packages/react/src/components/TableHandles/hooks/useTableHandlesData.ts diff --git a/examples/editor/examples/basic/App.tsx b/examples/editor/examples/basic/App.tsx index 99e7379a3c..bfce7bf560 100644 --- a/examples/editor/examples/basic/App.tsx +++ b/examples/editor/examples/basic/App.tsx @@ -13,7 +13,7 @@ import { BlockNoteView, createReactInlineContentSpec, DefaultPositionedSuggestionMenu, - SuggestionMenuItemProps, + MantineSuggestionMenuItemProps, useBlockNote, } from "@blocknote/react"; import "@blocknote/react/style.css"; @@ -57,9 +57,9 @@ async function getMentionMenuItems( query: string, closeMenu: () => void, clearQuery: () => void -): Promise { +): Promise { const users = ["Steve", "Bob", "Joe", "Mike"]; - const items: SuggestionMenuItemProps[] = users.map((user) => ({ + const items: MantineSuggestionMenuItemProps[] = users.map((user) => ({ name: user, execute: () => { closeMenu(); diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 0a2d6af1a2..80919412bd 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -41,7 +41,6 @@ import { BlockNoteDOMAttributes, BlockSchema, BlockSchemaFromSpecs, - BlockSchemaWithBlock, BlockSpecs, InlineContentSchema, InlineContentSchemaFromSpecs, @@ -222,16 +221,7 @@ export class BlockNoteEditor< SSchema >; public readonly tableHandles: - | TableHandlesProsemirrorPlugin< - BSchema extends BlockSchemaWithBlock< - "table", - DefaultBlockSchema["table"] - > - ? BSchema - : any, - ISchema, - SSchema - > + | TableHandlesProsemirrorPlugin | undefined; public readonly suggestionMenus: SuggestionMenuProseMirrorPlugin< diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index 520ae5718e..67fd684bfb 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -9,7 +9,6 @@ import { BlockFromConfigNoChildren, BlockSchemaWithBlock, InlineContentSchema, - PartialBlock, SpecificBlock, StyleSchema, } from "../../schema"; @@ -17,26 +16,6 @@ import { getDraggableBlockFromCoords } from "../SideMenu/SideMenuPlugin"; let dragImageElement: HTMLElement | undefined; -function setHiddenDragImage() { - if (dragImageElement) { - return; - } - - dragImageElement = document.createElement("div"); - dragImageElement.innerHTML = "_"; - dragImageElement.style.opacity = "0"; - dragImageElement.style.height = "1px"; - dragImageElement.style.width = "1px"; - document.body.appendChild(dragImageElement); -} - -function unsetHiddenDragImage() { - if (dragImageElement) { - document.body.removeChild(dragImageElement); - dragImageElement = undefined; - } -} - export type TableHandlesState< I extends InlineContentSchema, S extends StyleSchema @@ -58,6 +37,26 @@ export type TableHandlesState< | undefined; }; +function setHiddenDragImage() { + if (dragImageElement) { + return; + } + + dragImageElement = document.createElement("div"); + dragImageElement.innerHTML = "_"; + dragImageElement.style.opacity = "0"; + dragImageElement.style.height = "1px"; + dragImageElement.style.width = "1px"; + document.body.appendChild(dragImageElement); +} + +function unsetHiddenDragImage() { + if (dragImageElement) { + document.body.removeChild(dragImageElement); + dragImageElement = undefined; + } +} + function getChildIndex(node: Element) { return Array.prototype.indexOf.call(node.parentElement!.childNodes, node); } @@ -91,7 +90,7 @@ export class TableHandlesView< > implements PluginView { public state?: TableHandlesState; - public updateState: () => void; + public emitUpdate: () => void; public tableId: string | undefined; public tablePos: number | undefined; @@ -101,16 +100,20 @@ export class TableHandlesView< public prevWasEditable: boolean | null = null; constructor( - private readonly editor: BlockNoteEditor, + private readonly editor: BlockNoteEditor< + BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + I, + S + >, private readonly pmView: EditorView, - updateState: (state: TableHandlesState) => void + emitUpdate: (state: TableHandlesState) => void ) { - this.updateState = () => { + this.emitUpdate = () => { if (!this.state) { throw new Error("Attempting to update uninitialized image toolbar"); } - updateState(this.state); + emitUpdate(this.state); }; pmView.dom.addEventListener("mousemove", this.mouseMoveHandler); @@ -131,7 +134,7 @@ export class TableHandlesView< if (!target || !this.editor.isEditable) { if (this.state?.show) { this.state.show = false; - this.updateState(); + this.emitUpdate(); } return; } @@ -196,7 +199,7 @@ export class TableHandlesView< draggingState: undefined, }; - this.updateState(); + this.emitUpdate(); return false; }; @@ -286,7 +289,7 @@ export class TableHandlesView< // Emits a state update if any of the fields have changed. if (emitStateUpdate) { - this.updateState(); + this.emitUpdate(); } // Dispatches a dummy transaction to force a decorations update if @@ -327,7 +330,7 @@ export class TableHandlesView< type: "tableContent", rows: rows, }, - } as PartialBlock); + }); }; scrollHandler = () => { @@ -343,7 +346,7 @@ export class TableHandlesView< this.state.referencePosTable = tableElement.getBoundingClientRect(); this.state.referencePosCell = cellElement.getBoundingClientRect(); - this.updateState(); + this.emitUpdate(); } }; @@ -360,14 +363,25 @@ export class TableHandlesView< export const tableHandlesPluginKey = new PluginKey("TableHandlesPlugin"); export class TableHandlesProsemirrorPlugin< - BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, I extends InlineContentSchema, S extends StyleSchema > extends EventEmitter { - private view: TableHandlesView | undefined; + private view: + | TableHandlesView< + BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + I, + S + > + | undefined; public readonly plugin: Plugin; - constructor(private readonly editor: BlockNoteEditor) { + constructor( + private readonly editor: BlockNoteEditor< + BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + I, + S + > + ) { super(); this.plugin = new Plugin({ key: tableHandlesPluginKey, @@ -531,7 +545,7 @@ export class TableHandlesProsemirrorPlugin< originalIndex: this.view!.state.colIndex, mousePos: event.clientX, }; - this.view!.updateState(); + this.view!.emitUpdate(); this.editor._tiptapEditor.view.dispatch( this.editor._tiptapEditor.state.tr.setMeta(tableHandlesPluginKey, { @@ -567,7 +581,7 @@ export class TableHandlesProsemirrorPlugin< originalIndex: this.view!.state.rowIndex, mousePos: event.clientY, }; - this.view!.updateState(); + this.view!.emitUpdate(); this.editor._tiptapEditor.view.dispatch( this.editor._tiptapEditor.state.tr.setMeta(tableHandlesPluginKey, { @@ -596,7 +610,7 @@ export class TableHandlesProsemirrorPlugin< } this.view!.state.draggingState = undefined; - this.view!.updateState(); + this.view!.emitUpdate(); this.editor._tiptapEditor.view.dispatch( this.editor._tiptapEditor.state.tr.setMeta(tableHandlesPluginKey, null) @@ -609,11 +623,15 @@ export class TableHandlesProsemirrorPlugin< * Freezes the drag handles. When frozen, they will stay attached to the same * cell regardless of which cell is hovered by the mouse cursor. */ - freezeHandles = () => (this.view!.menuFrozen = true); + freezeHandles = () => { + this.view!.menuFrozen = true; + }; /** * Unfreezes the drag handles. When frozen, they will stay attached to the * same cell regardless of which cell is hovered by the mouse cursor. */ - unfreezeHandles = () => (this.view!.menuFrozen = false); + unfreezeHandles = () => { + this.view!.menuFrozen = false; + }; } diff --git a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx index 644736382b..86b668196f 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx @@ -1,6 +1,9 @@ import { BlockNoteEditor, BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, InlineContentSchema, StyleSchema, SuggestionMenuState, @@ -15,9 +18,9 @@ import { MantineSuggestionMenuProps } from "./MantineDefaults/MantineSuggestionM import { MantineSuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; export function DefaultPositionedSuggestionMenu< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema, Item extends { name: string; execute: () => void; diff --git a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx index 5a9a372d48..4907274e49 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx @@ -43,13 +43,13 @@ export type SuggestionMenuProps< >; export function DefaultSuggestionMenu< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema, Item extends { name: string; execute: () => void; - } + } = MantineSuggestionMenuItemProps >(props: SuggestionMenuProps) { const { editor, diff --git a/packages/react/src/components/TableHandles/BlockSchemaWithTable.ts b/packages/react/src/components/TableHandles/BlockSchemaWithTable.ts new file mode 100644 index 0000000000..92b2cf9fa4 --- /dev/null +++ b/packages/react/src/components/TableHandles/BlockSchemaWithTable.ts @@ -0,0 +1,6 @@ +import { BlockSchemaWithBlock, DefaultBlockSchema } from "@blocknote/core"; + +export type BlockSchemaWithTable = BlockSchemaWithBlock< + "table", + DefaultBlockSchema["table"] +>; diff --git a/packages/react/src/components/TableHandles/DefaultPositionedTableHandles.tsx b/packages/react/src/components/TableHandles/DefaultPositionedTableHandles.tsx new file mode 100644 index 0000000000..df2ebc9fe1 --- /dev/null +++ b/packages/react/src/components/TableHandles/DefaultPositionedTableHandles.tsx @@ -0,0 +1,115 @@ +import { + BlockNoteEditor, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, + TableHandlesState, +} from "@blocknote/core"; +import { DragEvent, FC, useState } from "react"; + +import { DragHandleMenuProps } from "../SideMenu/DragHandleMenu/DragHandleMenu"; +import { useUiElement } from "../../hooks/useUiElement"; +import { useTableHandlesPosition } from "./hooks/useTableHandlesPosition"; +import { DefaultTableHandle } from "./DefaultTableHandle"; +import { BlockSchemaWithTable } from "./BlockSchemaWithTable"; + +type NonUndefined = T extends undefined ? never : T; + +export type TableHandleProps< + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +> = { + editor: BlockNoteEditor; + orientation: "row" | "column"; + index: number; + dragStart: (e: DragEvent) => void; + showOtherSide: () => void; + hideOtherSide: () => void; + tableHandleMenu?: FC>; +} & Pick, "block"> & + Pick< + NonUndefined["tableHandles"]>, + "dragEnd" | "freezeHandles" | "unfreezeHandles" + >; + +export const DefaultPositionedTableHandles = < + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(props: { + editor: BlockNoteEditor; + tableHandle?: FC>; +}) => { + const callbacks = { + rowDragStart: props.editor.tableHandles!.rowDragStart, + colDragStart: props.editor.tableHandles!.colDragStart, + dragEnd: props.editor.tableHandles!.dragEnd, + freezeHandles: props.editor.tableHandles!.freezeHandles, + unfreezeHandles: props.editor.tableHandles!.unfreezeHandles, + }; + + const state = useUiElement( + props.editor.tableHandles!.onUpdate.bind(props.editor.tableHandles) + ); + const { rowHandle, colHandle } = useTableHandlesPosition( + state?.show || false, + state?.referencePosCell || null, + state?.referencePosTable || null, + state?.draggingState + ? { + draggedCellOrientation: state?.draggingState?.draggedCellOrientation, + mousePos: state?.draggingState?.mousePos, + } + : undefined + ); + console.log(state); + console.log(rowHandle); + console.log(colHandle); + + const [hideRow, setHideRow] = useState(false); + const [hideCol, setHideCol] = useState(false); + + if (!rowHandle.isMounted || !colHandle.isMounted || !state) { + return null; + } + + const TableHandle = props.tableHandle || DefaultTableHandle; + + return ( + <> + {!hideRow && ( +
+ setHideCol(false)} + hideOtherSide={() => setHideCol(true)} + index={state.rowIndex} + block={state.block} + dragStart={callbacks.rowDragStart} + dragEnd={callbacks.dragEnd} + freezeHandles={callbacks.freezeHandles} + unfreezeHandles={callbacks.unfreezeHandles} + /> +
+ )} + {!hideCol && ( +
+ setHideRow(false)} + hideOtherSide={() => setHideRow(true)} + index={state.colIndex} + block={state.block} + dragStart={callbacks.colDragStart} + dragEnd={callbacks.dragEnd} + freezeHandles={callbacks.freezeHandles} + unfreezeHandles={callbacks.unfreezeHandles} + /> +
+ )} + + ); +}; diff --git a/packages/react/src/components/TableHandles/DefaultTableHandle.tsx b/packages/react/src/components/TableHandles/DefaultTableHandle.tsx index f0050ea81e..c59aa7e031 100644 --- a/packages/react/src/components/TableHandles/DefaultTableHandle.tsx +++ b/packages/react/src/components/TableHandles/DefaultTableHandle.tsx @@ -1,12 +1,18 @@ -import { BlockSchemaWithBlock, DefaultBlockSchema } from "@blocknote/core"; +import { + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; import { MdDragIndicator } from "react-icons/md"; import { TableHandle } from "./TableHandle"; -import type { TableHandleProps } from "./TableHandlePositioner"; +import type { TableHandleProps } from "./DefaultPositionedTableHandles"; export const DefaultTableHandle = < - BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]> + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema >( - props: TableHandleProps + props: TableHandleProps ) => ( diff --git a/packages/react/src/components/TableHandles/TableHandle.tsx b/packages/react/src/components/TableHandles/TableHandle.tsx index 1bd948e3e2..555226ad7b 100644 --- a/packages/react/src/components/TableHandles/TableHandle.tsx +++ b/packages/react/src/components/TableHandles/TableHandle.tsx @@ -1,17 +1,21 @@ import { - BlockSchemaWithBlock, - DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, mergeCSSClasses, + StyleSchema, } from "@blocknote/core"; import { Menu } from "@mantine/core"; import { ReactNode, useState } from "react"; + +import type { TableHandleProps } from "./DefaultPositionedTableHandles"; import { DefaultTableHandleMenu } from "./TableHandleMenu/DefaultTableHandleMenu"; -import type { TableHandleProps } from "./TableHandlePositioner"; export const TableHandle = < - BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]> + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema >( - props: TableHandleProps & { children: ReactNode } + props: TableHandleProps & { children: ReactNode } ) => { const TableHandleMenu = props.tableHandleMenu || DefaultTableHandleMenu; diff --git a/packages/react/src/components/TableHandles/TableHandlePositioner.tsx b/packages/react/src/components/TableHandles/TableHandlePositioner.tsx deleted file mode 100644 index 66ca7b5143..0000000000 --- a/packages/react/src/components/TableHandles/TableHandlePositioner.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import { - BlockFromConfigNoChildren, - BlockNoteEditor, - BlockSchemaWithBlock, - DefaultBlockSchema, - InlineContentSchema, - StyleSchema, - TableHandlesProsemirrorPlugin, - TableHandlesState, -} from "@blocknote/core"; -import { offset, useFloating, useTransitionStyles } from "@floating-ui/react"; -import { DragEvent, FC, useEffect, useRef, useState } from "react"; - -import { DragHandleMenuProps } from "../SideMenu/DragHandleMenu/DragHandleMenu"; -import { DefaultTableHandle } from "./DefaultTableHandle"; - -export type TableHandleProps< - BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, - I extends InlineContentSchema, - S extends StyleSchema -> = Pick< - TableHandlesProsemirrorPlugin, - "dragEnd" | "freezeHandles" | "unfreezeHandles" -> & - Omit< - TableHandlesState, - | "rowIndex" - | "colIndex" - | "referencePosCell" - | "referencePosTable" - | "show" - | "draggingState" - > & { - orientation: "row" | "column"; - editor: BlockNoteEditor< - BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]> - >; - tableHandleMenu?: FC>; - dragStart: (e: DragEvent) => void; - index: number; - // TODO: document this, explain why we need it - showOtherSide: () => void; - hideOtherSide: () => void; - }; - -export const TableHandlesPositioner = < - BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, - I extends InlineContentSchema, - S extends StyleSchema ->(props: { - editor: BlockNoteEditor; - tableHandle?: FC>; -}) => { - const [show, setShow] = useState(false); - const [hideRow, setHideRow] = useState(false); - const [hideCol, setHideCol] = useState(false); - const [block, setBlock] = - useState>(); - - const [rowIndex, setRowIndex] = useState(); - const [colIndex, setColIndex] = useState(); - - const [draggedCellOrientation, setDraggedCellOrientation] = useState< - "row" | "col" | undefined - >(undefined); - const [mousePos, setMousePos] = useState(); - - const [, setForceUpdate] = useState(0); - - const referencePosCell = useRef(); - const referencePosTable = useRef(); - - const rowFloating = useFloating({ - open: show, - placement: "left", - middleware: [offset(-10)], - }); - const colFloating = useFloating({ - open: show, - placement: "top", - middleware: [offset(-12)], - }); - - const rowTransitionStyles = useTransitionStyles(rowFloating.context); - const colTransitionStyles = useTransitionStyles(colFloating.context); - - useEffect(() => { - return props.editor.tableHandles!.onUpdate((state) => { - // console.log("update", state.draggingState); - setShow(state.show); - setBlock(state.block); - setRowIndex(state.rowIndex); - setColIndex(state.colIndex); - - if (state.draggingState) { - setDraggedCellOrientation(state.draggingState.draggedCellOrientation); - setMousePos(state.draggingState.mousePos); - } else { - setDraggedCellOrientation(undefined); - setMousePos(undefined); - } - - setForceUpdate(Math.random()); - - referencePosCell.current = state.referencePosCell; - referencePosTable.current = state.referencePosTable; - - rowFloating.update(); - colFloating.update(); - }); - }, [colFloating, props.editor, rowFloating]); - - useEffect(() => { - rowFloating.refs.setReference({ - getBoundingClientRect: () => { - if (draggedCellOrientation === "row") { - return new DOMRect( - referencePosTable.current!.x, - mousePos!, - referencePosTable.current!.width, - 0 - ); - } - - return new DOMRect( - referencePosTable.current!.x, - referencePosCell.current!.y, - referencePosTable.current!.width, - referencePosCell.current!.height - ); - }, - }); - }, [draggedCellOrientation, mousePos, rowFloating.refs]); - - useEffect(() => { - colFloating.refs.setReference({ - getBoundingClientRect: () => { - if (draggedCellOrientation === "col") { - return new DOMRect( - mousePos!, - referencePosTable.current!.y, - 0, - referencePosTable.current!.height - ); - } - - return new DOMRect( - referencePosCell.current!.x, - referencePosTable.current!.y, - referencePosCell.current!.width, - referencePosTable.current!.height - ); - }, - }); - }, [colFloating.refs, draggedCellOrientation, mousePos]); - - const TableHandle = props.tableHandle || DefaultTableHandle; - - if ( - !rowTransitionStyles.isMounted || - !colTransitionStyles.isMounted || - (hideRow && hideCol) - ) { - return null; - } - - return ( - <> - {!hideRow && ( -
- setHideCol(false)} - hideOtherSide={() => setHideCol(true)} - /> -
- )} - {!hideCol && ( -
- setHideRow(false)} - hideOtherSide={() => setHideRow(true)} - /> -
- )} - - ); -}; diff --git a/packages/react/src/components/TableHandles/hooks/useTableHandlesData.ts b/packages/react/src/components/TableHandles/hooks/useTableHandlesData.ts deleted file mode 100644 index 476d92ea91..0000000000 --- a/packages/react/src/components/TableHandles/hooks/useTableHandlesData.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - BlockFromConfigNoChildren, - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - InlineContentSchema, - StyleSchema, - TableHandlesData, -} from "@blocknote/core"; -import { useEffect, useState } from "react"; -import { UiComponentData } from "../../../components-shared/UiComponentTypes"; - -export function useTableHandlesData< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->( - editor: BlockNoteEditor -): UiComponentData, "tableHandles"> { - const [block, setBlock] = - useState>(); - const [rowIndex, setRowIndex] = useState(); - const [colIndex, setColIndex] = useState(); - const [draggedCellOrientation, setDraggedCellOrientation] = useState< - "row" | "col" | undefined - >(undefined); - - useEffect(() => { - return editor.tableHandles!.onDataUpdate((data) => { - setBlock(data.block); - setRowIndex(data.rowIndex); - setColIndex(data.colIndex); - - if (data.draggingState) { - setDraggedCellOrientation(data.draggingState.draggedCellOrientation); - } else { - setDraggedCellOrientation(undefined); - } - }); - }, [editor]); - - return { - block: block!, - rowIndex: rowIndex!, - colIndex: colIndex!, - draggingState: draggedCellOrientation, - rowDragStart: editor.tableHandles!.rowDragStart, - colDragStart: editor.tableHandles!.colDragStart, - dragEnd: editor.tableHandles!.dragEnd, - freezeHandles: editor.tableHandles!.freezeHandles, - unfreezeHandles: editor.tableHandles!.unfreezeHandles, - } as any; -} diff --git a/packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts b/packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts index c5700c81bb..52c8bf47a7 100644 --- a/packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts +++ b/packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts @@ -1,139 +1,148 @@ -import { - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; -import { useEffect, useRef, useState } from "react"; import { offset, useFloating, useTransitionStyles } from "@floating-ui/react"; +import { useEffect, useMemo, useState } from "react"; -export function useTableHandlesPosition< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->( - editor: BlockNoteEditor -): { - rowHandle: UiComponentPosition; - colHandle: UiComponentPosition; -} { - const [show, setShow] = useState(false); +import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; - const [draggedCellOrientation, setDraggedCellOrientation] = useState< - "row" | "col" | undefined - >(undefined); - const [mousePos, setMousePos] = useState(); +function getBoundingClientRectRow( + referencePosCell: DOMRect | null, + referencePosTable: DOMRect | null, + draggingState?: { + draggedCellOrientation: "row" | "col"; + mousePos: number; + } +) { + if (draggingState && draggingState.draggedCellOrientation === "row") { + return new DOMRect( + referencePosTable!.x, + draggingState.mousePos, + referencePosTable!.width, + 0 + ); + } - const [, setForceUpdate] = useState(0); + return new DOMRect( + referencePosTable!.x, + referencePosCell!.y, + referencePosTable!.width, + referencePosCell!.height + ); +} - const referencePosCell = useRef(); - const referencePosTable = useRef(); +function getBoundingClientRectCol( + referencePosCell: DOMRect | null, + referencePosTable: DOMRect | null, + draggingState?: { + draggedCellOrientation: "row" | "col"; + mousePos: number; + } +) { + if (draggingState && draggingState.draggedCellOrientation === "col") { + return new DOMRect( + draggingState.mousePos, + referencePosTable!.y, + 0, + referencePosTable!.height + ); + } - const rowFloating = useFloating({ - open: show, - placement: "left", - middleware: [offset(-10)], - }); - const colFloating = useFloating({ + return new DOMRect( + referencePosCell!.x, + referencePosTable!.y, + referencePosCell!.width, + referencePosTable!.height + ); +} + +function useTableHandlePosition( + orientation: "row" | "col", + show: boolean, + referencePosCell: DOMRect | null, + referencePosTable: DOMRect | null, + draggingState?: { + draggedCellOrientation: "row" | "col"; + mousePos: number; + } +): UiComponentPosition { + // TODO: What is this for? Disabling it doesn't seem to break anything + const [, setForceUpdate] = useState(0); + + const { refs, update, context, floatingStyles } = useFloating({ open: show, - placement: "top", - middleware: [offset(-12)], + placement: orientation === "row" ? "left" : "top", + middleware: [offset(orientation === "row" ? -10 : -12)], }); - const rowTransitionStyles = useTransitionStyles(rowFloating.context); - const colTransitionStyles = useTransitionStyles(colFloating.context); + const { isMounted, styles } = useTransitionStyles(context); useEffect(() => { - return editor.tableHandles!.onPositionUpdate((position) => { - // console.log("update", state.draggingState); - setShow(position.show); - setForceUpdate(Math.random()); - - if (position.draggingState) { - setDraggedCellOrientation( - position.draggingState.draggedCellOrientation - ); - setMousePos(position.draggingState.mousePos); - } else { - setDraggedCellOrientation(undefined); - setMousePos(undefined); - } - - referencePosCell.current = position.referencePosCell; - referencePosTable.current = position.referencePosTable; - rowFloating.update(); - colFloating.update(); - }); - }, [colFloating, editor.tableHandles, rowFloating]); + setForceUpdate(Math.random()); + update(); + }, [referencePosCell, referencePosTable, update]); useEffect(() => { - rowFloating.refs.setReference({ - getBoundingClientRect: () => { - if (draggedCellOrientation === "row") { - return new DOMRect( - referencePosTable.current!.x, - mousePos!, - referencePosTable.current!.width, - 0 - ); - } + // TODO: Maybe throw error instead if null + if (referencePosCell === null || referencePosTable === null) { + return; + } - return new DOMRect( - referencePosTable.current!.x, - referencePosCell.current!.y, - referencePosTable.current!.width, - referencePosCell.current!.height - ); - }, - }); - }, [draggedCellOrientation, mousePos, rowFloating.refs]); - - useEffect(() => { - colFloating.refs.setReference({ + refs.setReference({ getBoundingClientRect: () => { - if (draggedCellOrientation === "col") { - return new DOMRect( - mousePos!, - referencePosTable.current!.y, - 0, - referencePosTable.current!.height - ); - } - - return new DOMRect( - referencePosCell.current!.x, - referencePosTable.current!.y, - referencePosCell.current!.width, - referencePosTable.current!.height - ); + const fn = + orientation === "row" + ? getBoundingClientRectRow + : getBoundingClientRectCol; + return fn(referencePosCell, referencePosTable, draggingState); }, }); - }, [colFloating.refs, draggedCellOrientation, mousePos]); + }, [draggingState, orientation, referencePosCell, referencePosTable, refs]); - return { - rowHandle: { - isMounted: rowTransitionStyles.isMounted, - ref: rowFloating.refs.setFloating, + return useMemo( + () => ({ + isMounted: isMounted, + ref: refs.setFloating, style: { display: "flex", - ...rowTransitionStyles.styles, - ...rowFloating.floatingStyles, + ...styles, + ...floatingStyles, zIndex: 10000, }, - }, - colHandle: { - isMounted: colTransitionStyles.isMounted, - ref: colFloating.refs.setFloating, - style: { - display: "flex", - ...colTransitionStyles.styles, - ...colFloating.floatingStyles, - zIndex: 10000, - }, - }, - }; + }), + [floatingStyles, isMounted, refs.setFloating, styles] + ); +} + +export function useTableHandlesPosition( + show: boolean, + referencePosCell: DOMRect | null, + referencePosTable: DOMRect | null, + draggingState?: { + draggedCellOrientation: "row" | "col"; + mousePos: number; + } +): { + rowHandle: UiComponentPosition; + colHandle: UiComponentPosition; +} { + const rowHandle = useTableHandlePosition( + "row", + show, + referencePosCell, + referencePosTable, + draggingState + ); + const colHandle = useTableHandlePosition( + "col", + show, + referencePosCell, + referencePosTable, + draggingState + ); + + return useMemo( + () => ({ + rowHandle, + colHandle, + }), + [colHandle, rowHandle] + ); } diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 9799f15be9..7a7198a8ac 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -3,7 +3,7 @@ import { DefaultPositionedHyperlinkToolbar } from "../components/HyperlinkToolba import { DefaultPositionedSuggestionMenu } from "../components/SuggestionMenu/DefaultPositionedSuggestionMenu"; import { DefaultPositionedSideMenu } from "../components/SideMenu/DefaultPositionedSideMenu"; import { DefaultPositionedImageToolbar } from "../components/ImageToolbar/DefaultPositionedImageToolbar"; -import { TableHandlesPositioner } from "../components/TableHandles/TableHandlePositioner"; +import { DefaultPositionedTableHandles } from "../components/TableHandles/DefaultPositionedTableHandles"; import { BlockNoteEditor, BlockSchema, @@ -42,7 +42,7 @@ export function BlockNoteDefaultUI< )} {props.editor.blockSchema.table && props.tableHandles !== false && ( - + )} ); diff --git a/packages/react/src/hooks/useUiElementPosition.ts b/packages/react/src/hooks/useUiElementPosition.ts index ddb7cc7e09..119deac211 100644 --- a/packages/react/src/hooks/useUiElementPosition.ts +++ b/packages/react/src/hooks/useUiElementPosition.ts @@ -4,6 +4,7 @@ import { useTransitionStyles, } from "@floating-ui/react"; import { useEffect, useMemo } from "react"; + import { UiComponentPosition } from "../components-shared/UiComponentTypes"; export function useUiElementPosition( @@ -25,11 +26,13 @@ export function useUiElementPosition( useEffect(() => { // TODO: Maybe throw error instead if null - if (referencePos !== null) { - refs.setReference({ - getBoundingClientRect: () => referencePos, - }); + if (referencePos === null) { + return; } + + refs.setReference({ + getBoundingClientRect: () => referencePos, + }); }, [referencePos, refs]); return useMemo( @@ -43,6 +46,6 @@ export function useUiElementPosition( zIndex: zIndex, }, }), - [isMounted, styles, floatingStyles, zIndex, refs] + [floatingStyles, isMounted, refs.setFloating, styles, zIndex] ); } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 7510a00536..c24e1597ef 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -43,10 +43,9 @@ export * from "./components/SuggestionMenu/DefaultPositionedSuggestionMenu"; export * from "./components/ImageToolbar/DefaultImageToolbar"; export * from "./components/ImageToolbar/DefaultPositionedImageToolbar"; -export * from "./components/TableHandles/hooks/useTableHandlesData"; export * from "./components/TableHandles/hooks/useTableHandlesPosition"; export * from "./components/TableHandles/DefaultTableHandle"; -export * from "./components/TableHandles/TableHandlePositioner"; +export * from "./components/TableHandles/DefaultPositionedTableHandles"; export * from "./components-shared/Toolbar/Toolbar"; export * from "./components-shared/Toolbar/ToolbarButton"; diff --git a/tests/src/utils/customblocks/Alert.tsx b/tests/src/utils/customblocks/Alert.tsx index 083ad4e5d3..c98520845f 100644 --- a/tests/src/utils/customblocks/Alert.tsx +++ b/tests/src/utils/customblocks/Alert.tsx @@ -7,7 +7,7 @@ import { StyleSchema, } from "@blocknote/core"; import { RiAlertFill } from "react-icons/ri"; -import { SuggestionMenuItemProps } from "@blocknote/react"; +import { MantineSuggestionMenuItemProps } from "@blocknote/react"; const values = { warning: { icon: "⚠️", @@ -164,4 +164,4 @@ export const insertAlert = < "success", ], group: "Other", - } satisfies SuggestionMenuItemProps); + } satisfies MantineSuggestionMenuItemProps); diff --git a/tests/src/utils/customblocks/Button.tsx b/tests/src/utils/customblocks/Button.tsx index 97b9f65e28..0421a5d453 100644 --- a/tests/src/utils/customblocks/Button.tsx +++ b/tests/src/utils/customblocks/Button.tsx @@ -6,7 +6,7 @@ import { InlineContentSchema, StyleSchema, } from "@blocknote/core"; -import { SuggestionMenuItemProps } from "@blocknote/react"; +import { MantineSuggestionMenuItemProps } from "@blocknote/react"; import { RiRadioButtonFill } from "react-icons/ri"; export const Button = createBlockSpec( @@ -70,4 +70,4 @@ export const insertButton = < icon: , aliases: ["button", "click", "action"], group: "Other", - } satisfies SuggestionMenuItemProps); + } satisfies MantineSuggestionMenuItemProps); diff --git a/tests/src/utils/customblocks/Embed.tsx b/tests/src/utils/customblocks/Embed.tsx index 1d9507c9d7..b5456dcda0 100644 --- a/tests/src/utils/customblocks/Embed.tsx +++ b/tests/src/utils/customblocks/Embed.tsx @@ -6,7 +6,7 @@ import { PartialBlock, StyleSchema, } from "@blocknote/core"; -import { SuggestionMenuItemProps } from "@blocknote/react"; +import { MantineSuggestionMenuItemProps } from "@blocknote/react"; import { RiLayout5Fill } from "react-icons/ri"; export const Embed = createBlockSpec( @@ -68,4 +68,4 @@ export const insertEmbed = < icon: , aliases: ["embedded", "website", "site", "link", "url"], group: "Other", - } satisfies SuggestionMenuItemProps); + } satisfies MantineSuggestionMenuItemProps); diff --git a/tests/src/utils/customblocks/Image.tsx b/tests/src/utils/customblocks/Image.tsx index be8dfed8ef..3d8419704c 100644 --- a/tests/src/utils/customblocks/Image.tsx +++ b/tests/src/utils/customblocks/Image.tsx @@ -7,7 +7,7 @@ import { PartialBlock, StyleSchema, } from "@blocknote/core"; -import { SuggestionMenuItemProps } from "@blocknote/react"; +import { MantineSuggestionMenuItemProps } from "@blocknote/react"; import { RiImage2Fill } from "react-icons/ri"; export const Image = createBlockSpec( { @@ -87,4 +87,4 @@ export const insertImage = < icon: , aliases: ["image", "img", "picture", "media"], group: "Other", - } satisfies SuggestionMenuItemProps); + } satisfies MantineSuggestionMenuItemProps); diff --git a/tests/src/utils/customblocks/ReactAlert.tsx b/tests/src/utils/customblocks/ReactAlert.tsx index f50739c5db..7a61360100 100644 --- a/tests/src/utils/customblocks/ReactAlert.tsx +++ b/tests/src/utils/customblocks/ReactAlert.tsx @@ -7,7 +7,7 @@ import { } from "@blocknote/core"; import { createReactBlockSpec, - SuggestionMenuItemProps, + MantineSuggestionMenuItemProps, } from "@blocknote/react"; import { useEffect, useState } from "react"; import { RiAlertFill } from "react-icons/ri"; @@ -162,4 +162,4 @@ export const insertReactAlert = < "success", ], group: "Other", - } satisfies SuggestionMenuItemProps); + } satisfies MantineSuggestionMenuItemProps); diff --git a/tests/src/utils/customblocks/ReactImage.tsx b/tests/src/utils/customblocks/ReactImage.tsx index ed6753bc02..1189c00546 100644 --- a/tests/src/utils/customblocks/ReactImage.tsx +++ b/tests/src/utils/customblocks/ReactImage.tsx @@ -8,7 +8,7 @@ import { } from "@blocknote/core"; import { createReactBlockSpec, - SuggestionMenuItemProps, + MantineSuggestionMenuItemProps, } from "@blocknote/react"; import { RiImage2Fill } from "react-icons/ri"; @@ -88,4 +88,4 @@ export const insertReactImage = < "media", ], group: "Media", - } satisfies SuggestionMenuItemProps); + } satisfies MantineSuggestionMenuItemProps); diff --git a/tests/src/utils/customblocks/Separator.tsx b/tests/src/utils/customblocks/Separator.tsx index aaf057377b..0f7bcaeae2 100644 --- a/tests/src/utils/customblocks/Separator.tsx +++ b/tests/src/utils/customblocks/Separator.tsx @@ -5,7 +5,7 @@ import { InlineContentSchema, StyleSchema, } from "@blocknote/core"; -import { SuggestionMenuItemProps } from "@blocknote/react"; +import { MantineSuggestionMenuItemProps } from "@blocknote/react"; import { RiSeparator } from "react-icons/ri"; export const Separator = createBlockSpec( @@ -65,4 +65,4 @@ export const insertSeparator = < icon: , aliases: ["separator", "horizontal", "line", "rule"], group: "Other", - } satisfies SuggestionMenuItemProps); + } satisfies MantineSuggestionMenuItemProps); From e0a7c2574ed1dc13536f3feb69903a8450797400 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 15 Feb 2024 14:27:10 +0100 Subject: [PATCH 7/7] Implemented PR feedback --- .../DefaultPositionedUiElement.tsx | 57 ------------------- .../DefaultPositionedFormattingToolbar.tsx | 8 +-- .../DefaultPositionedHyperlinkToolbar.tsx | 8 +-- .../DefaultPositionedImageToolbar.tsx | 8 +-- .../SideMenu/DefaultPositionedSideMenu.tsx | 8 +-- .../DefaultPositionedSuggestionMenu.tsx | 17 +++--- .../DefaultPositionedTableHandles.tsx | 8 +-- ...ition.ts => useTableHandlesPositioning.ts} | 8 +-- .../{useUiElement.ts => useUIPluginState.ts} | 2 +- ...Position.ts => useUiElementPositioning.ts} | 2 +- packages/react/src/index.ts | 2 +- 11 files changed, 34 insertions(+), 94 deletions(-) delete mode 100644 packages/react/src/components-shared/DefaultPositionedUiElement.tsx rename packages/react/src/components/TableHandles/hooks/{useTableHandlesPosition.ts => useTableHandlesPositioning.ts} (92%) rename packages/react/src/hooks/{useUiElement.ts => useUIPluginState.ts} (87%) rename packages/react/src/hooks/{useUiElementPosition.ts => useUiElementPositioning.ts} (96%) diff --git a/packages/react/src/components-shared/DefaultPositionedUiElement.tsx b/packages/react/src/components-shared/DefaultPositionedUiElement.tsx deleted file mode 100644 index 0d5d4af9b3..0000000000 --- a/packages/react/src/components-shared/DefaultPositionedUiElement.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { - BlockNoteEditor, - BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - InlineContentSchema, - StyleSchema, - UiElementPosition, -} from "@blocknote/core"; -import { flip, offset, UseFloatingOptions } from "@floating-ui/react"; -import { FC } from "react"; -import { useUiElement } from "../hooks/useUiElement"; -import { useUiElementPosition } from "../hooks/useUiElementPosition"; - -export const DefaultPositionedUiElement = < - State extends UiElementPosition, - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->(props: { - editor: BlockNoteEditor; - onUpdate: (callback: (state: State) => void) => void; - zIndex: number; - options?: Partial; - component: FC< - { editor: BlockNoteEditor } & Omit< - State, - keyof UiElementPosition - > - >; -}) => { - const state = useUiElement(props.onUpdate); - const { isMounted, ref, style } = useUiElementPosition( - state?.show || false, - state?.referencePos || null, - 5000, - { - placement: "bottom", - middleware: [offset(10), flip()], - } - ); - - if (!isMounted || !state) { - return null; - } - - const { show, referencePos, ...rest } = state; - - const Component = props.component; - - return ( -
- -
- ); -}; diff --git a/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx b/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx index de4d894a52..e673f50375 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx @@ -7,8 +7,8 @@ import { import { flip, offset } from "@floating-ui/react"; import { FC, useState } from "react"; -import { useUiElement } from "../../hooks/useUiElement"; -import { useUiElementPosition } from "../../hooks/useUiElementPosition"; +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; import { useEditorChange } from "../../hooks/useEditorChange"; import { DefaultFormattingToolbar, @@ -64,10 +64,10 @@ export const DefaultPositionedFormattingToolbar = < } }); - const state = useUiElement( + const state = useUIPluginState( props.editor.formattingToolbar.onUpdate.bind(props.editor.formattingToolbar) ); - const { isMounted, ref, style } = useUiElementPosition( + const { isMounted, ref, style } = useUiElementPositioning( state?.show || false, state?.referencePos || null, 3000, diff --git a/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx b/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx index 36058f5563..63f62c782e 100644 --- a/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx +++ b/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx @@ -10,8 +10,8 @@ import { import { flip, offset } from "@floating-ui/react"; import { FC } from "react"; -import { useUiElement } from "../../hooks/useUiElement"; -import { useUiElementPosition } from "../../hooks/useUiElementPosition"; +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; import { DefaultHyperlinkToolbar, HyperlinkToolbarProps, @@ -32,10 +32,10 @@ export const DefaultPositionedHyperlinkToolbar = < stopHideTimer: props.editor.hyperlinkToolbar.stopHideTimer, }; - const state = useUiElement( + const state = useUIPluginState( props.editor.hyperlinkToolbar.onUpdate.bind(props.editor.hyperlinkToolbar) ); - const { isMounted, ref, style } = useUiElementPosition( + const { isMounted, ref, style } = useUiElementPositioning( state?.show || false, state?.referencePos || null, 4000, diff --git a/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx b/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx index bada0a6dff..3e8d757dbd 100644 --- a/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx +++ b/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx @@ -6,8 +6,8 @@ import { import { FC } from "react"; import { flip, offset } from "@floating-ui/react"; -import { useUiElement } from "../../hooks/useUiElement"; -import { useUiElementPosition } from "../../hooks/useUiElementPosition"; +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; import { DefaultImageToolbar, ImageToolbarProps } from "./DefaultImageToolbar"; export const DefaultPositionedImageToolbar = < @@ -16,10 +16,10 @@ export const DefaultPositionedImageToolbar = < editor: BlockNoteEditor; imageToolbar?: FC>; }) => { - const state = useUiElement( + const state = useUIPluginState( props.editor.imageToolbar.onUpdate.bind(props.editor.imageToolbar) ); - const { isMounted, ref, style } = useUiElementPosition( + const { isMounted, ref, style } = useUiElementPositioning( state?.show || false, state?.referencePos || null, 5000, diff --git a/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx index 1da0a668be..903ddb3e0d 100644 --- a/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx +++ b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx @@ -9,8 +9,8 @@ import { } from "@blocknote/core"; import { FC } from "react"; -import { useUiElement } from "../../hooks/useUiElement"; -import { useUiElementPosition } from "../../hooks/useUiElementPosition"; +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; import { DefaultSideMenu, SideMenuProps } from "./DefaultSideMenu"; export const DefaultPositionedSideMenu = < @@ -29,10 +29,10 @@ export const DefaultPositionedSideMenu = < unfreezeMenu: props.editor.sideMenu.unfreezeMenu, }; - const state = useUiElement( + const state = useUIPluginState( props.editor.sideMenu.onUpdate.bind(props.editor.sideMenu) ); - const { isMounted, ref, style } = useUiElementPosition( + const { isMounted, ref, style } = useUiElementPositioning( state?.show || false, state?.referencePos || null, 1000, diff --git a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx index 86b668196f..a00f2d6d6b 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx @@ -11,8 +11,8 @@ import { import { flip, offset, size } from "@floating-ui/react"; import { FC } from "react"; -import { useUiElement } from "../../hooks/useUiElement"; -import { useUiElementPosition } from "../../hooks/useUiElementPosition"; +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; import { DefaultSuggestionMenu } from "./DefaultSuggestionMenu"; import { MantineSuggestionMenuProps } from "./MantineDefaults/MantineSuggestionMenu"; import { MantineSuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; @@ -40,13 +40,14 @@ export function DefaultPositionedSuggestionMenu< clearQuery: props.editor.suggestionMenus.clearQuery, }; - const state = useUiElement((callback: (state: SuggestionMenuState) => void) => - props.editor.suggestionMenus.onUpdate.bind(props.editor.suggestionMenus)( - props.triggerCharacter || "/", - callback - ) + const state = useUIPluginState( + (callback: (state: SuggestionMenuState) => void) => + props.editor.suggestionMenus.onUpdate.bind(props.editor.suggestionMenus)( + props.triggerCharacter || "/", + callback + ) ); - const { isMounted, ref, style } = useUiElementPosition( + const { isMounted, ref, style } = useUiElementPositioning( state?.show || false, state?.referencePos || null, 2000, diff --git a/packages/react/src/components/TableHandles/DefaultPositionedTableHandles.tsx b/packages/react/src/components/TableHandles/DefaultPositionedTableHandles.tsx index df2ebc9fe1..e7882af7c1 100644 --- a/packages/react/src/components/TableHandles/DefaultPositionedTableHandles.tsx +++ b/packages/react/src/components/TableHandles/DefaultPositionedTableHandles.tsx @@ -9,8 +9,8 @@ import { import { DragEvent, FC, useState } from "react"; import { DragHandleMenuProps } from "../SideMenu/DragHandleMenu/DragHandleMenu"; -import { useUiElement } from "../../hooks/useUiElement"; -import { useTableHandlesPosition } from "./hooks/useTableHandlesPosition"; +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useTableHandlesPositioning } from "./hooks/useTableHandlesPositioning"; import { DefaultTableHandle } from "./DefaultTableHandle"; import { BlockSchemaWithTable } from "./BlockSchemaWithTable"; @@ -48,10 +48,10 @@ export const DefaultPositionedTableHandles = < unfreezeHandles: props.editor.tableHandles!.unfreezeHandles, }; - const state = useUiElement( + const state = useUIPluginState( props.editor.tableHandles!.onUpdate.bind(props.editor.tableHandles) ); - const { rowHandle, colHandle } = useTableHandlesPosition( + const { rowHandle, colHandle } = useTableHandlesPositioning( state?.show || false, state?.referencePosCell || null, state?.referencePosTable || null, diff --git a/packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts b/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts similarity index 92% rename from packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts rename to packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts index 52c8bf47a7..4f1406a81e 100644 --- a/packages/react/src/components/TableHandles/hooks/useTableHandlesPosition.ts +++ b/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts @@ -1,5 +1,5 @@ import { offset, useFloating, useTransitionStyles } from "@floating-ui/react"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo } from "react"; import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; @@ -63,9 +63,6 @@ function useTableHandlePosition( mousePos: number; } ): UiComponentPosition { - // TODO: What is this for? Disabling it doesn't seem to break anything - const [, setForceUpdate] = useState(0); - const { refs, update, context, floatingStyles } = useFloating({ open: show, placement: orientation === "row" ? "left" : "top", @@ -75,7 +72,6 @@ function useTableHandlePosition( const { isMounted, styles } = useTransitionStyles(context); useEffect(() => { - setForceUpdate(Math.random()); update(); }, [referencePosCell, referencePosTable, update]); @@ -111,7 +107,7 @@ function useTableHandlePosition( ); } -export function useTableHandlesPosition( +export function useTableHandlesPositioning( show: boolean, referencePosCell: DOMRect | null, referencePosTable: DOMRect | null, diff --git a/packages/react/src/hooks/useUiElement.ts b/packages/react/src/hooks/useUIPluginState.ts similarity index 87% rename from packages/react/src/hooks/useUiElement.ts rename to packages/react/src/hooks/useUIPluginState.ts index 35025b0a1a..c49ae72ea1 100644 --- a/packages/react/src/hooks/useUiElement.ts +++ b/packages/react/src/hooks/useUIPluginState.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -export function useUiElement( +export function useUIPluginState( onUpdate: (callback: (state: State) => void) => void ): State | undefined { const [state, setState] = useState(); diff --git a/packages/react/src/hooks/useUiElementPosition.ts b/packages/react/src/hooks/useUiElementPositioning.ts similarity index 96% rename from packages/react/src/hooks/useUiElementPosition.ts rename to packages/react/src/hooks/useUiElementPositioning.ts index 119deac211..d37950ed2d 100644 --- a/packages/react/src/hooks/useUiElementPosition.ts +++ b/packages/react/src/hooks/useUiElementPositioning.ts @@ -7,7 +7,7 @@ import { useEffect, useMemo } from "react"; import { UiComponentPosition } from "../components-shared/UiComponentTypes"; -export function useUiElementPosition( +export function useUiElementPositioning( show: boolean, referencePos: DOMRect | null, zIndex: number, diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index c24e1597ef..249fa885db 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -43,7 +43,7 @@ export * from "./components/SuggestionMenu/DefaultPositionedSuggestionMenu"; export * from "./components/ImageToolbar/DefaultImageToolbar"; export * from "./components/ImageToolbar/DefaultPositionedImageToolbar"; -export * from "./components/TableHandles/hooks/useTableHandlesPosition"; +export * from "./components/TableHandles/hooks/useTableHandlesPositioning"; export * from "./components/TableHandles/DefaultTableHandle"; export * from "./components/TableHandles/DefaultPositionedTableHandles";