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/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..c05fb389f4 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.onUpdate((state) => { + if (state.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 = 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 e495e601ec..9a9a218f91 100644 --- a/examples/vanilla/src/ui/addHyperlinkToolbar.ts +++ b/examples/vanilla/src/ui/addHyperlinkToolbar.ts @@ -2,43 +2,38 @@ 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); + 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); - const removeBtn = createButton("remove", () => { - editor.hyperlinkToolbar.deleteHyperlink(); - }); + const removeBtn = createButton("remove", () => { + editor.hyperlinkToolbar.deleteHyperlink(); + }); + element.appendChild(removeBtn); - element.appendChild(editBtn); - element.appendChild(removeBtn); - element.style.display = "none"; + document.getElementById("root")!.appendChild(element); - document.getElementById("root")!.appendChild(element); - } + editor.hyperlinkToolbar.onUpdate((state) => { + if (state.show) { + url = state.url; + text = state.text; - if (hyperlinkToolbarState.show) { element.style.display = "block"; - element.style.top = hyperlinkToolbarState.referencePos.top + "px"; - element.style.left = - hyperlinkToolbarState.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/addSideMenu.ts b/examples/vanilla/src/ui/addSideMenu.ts index de277aabcc..ecb9f4f91c 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.onUpdate((state) => { + if (state.show) { element.style.display = "block"; - element.style.top = sideMenuState.referencePos.top + "px"; - element.style.left = - sideMenuState.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 98355a43d4..a3f62e3bfe 100644 --- a/examples/vanilla/src/ui/addSlashMenu.ts +++ b/examples/vanilla/src/ui/addSlashMenu.ts @@ -2,7 +2,80 @@ 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 = [ + { + 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,95 +94,14 @@ export const addSlashMenu = async (editor: BlockNoteEditor) => { return domItems; } - editor.suggestionMenus.onUpdate("slashMenu", async (slashMenuState) => { - 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); - } - - 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); + editor.suggestionMenus.onUpdate("/", async (state) => { + if (state.show) { + await updateItems(state.query, getItems); element.style.display = "block"; - element.style.top = slashMenuState.referencePos.top + "px"; - element.style.left = - slashMenuState.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/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-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..5a04c3a39d 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -3,20 +3,15 @@ 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 { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { EventEmitter } from "../../util/EventEmitter"; -export type FormattingToolbarCallbacks = BaseUiElementCallbacks; - -export type FormattingToolbarState = BaseUiElementState; +export type FormattingToolbarState = UiElementPosition; export class FormattingToolbarView { - private formattingToolbarState?: FormattingToolbarState; - public updateFormattingToolbar: () => void; + public state?: FormattingToolbarState; + public emitUpdate: () => void; public preventHide = false; public preventShow = false; @@ -36,18 +31,16 @@ export class FormattingToolbarView { StyleSchema >, private readonly pmView: EditorView, - updateFormattingToolbar: ( - formattingToolbarState: FormattingToolbarState - ) => void + emitUpdate: (state: FormattingToolbarState) => void ) { - this.updateFormattingToolbar = () => { - if (!this.formattingToolbarState) { + this.emitUpdate = () => { + if (!this.state) { throw new Error( "Attempting to update uninitialized formatting toolbar" ); } - updateFormattingToolbar(this.formattingToolbarState); + emitUpdate(this.state); }; pmView.dom.addEventListener("mousedown", this.viewMousedownHandler); @@ -72,9 +65,9 @@ export class FormattingToolbarView { // For dragging the whole editor. dragHandler = () => { - if (this.formattingToolbarState?.show) { - this.formattingToolbarState.show = false; - this.updateFormattingToolbar(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } }; @@ -105,16 +98,16 @@ export class FormattingToolbarView { return; } - if (this.formattingToolbarState?.show) { - this.formattingToolbarState.show = false; - this.updateFormattingToolbar(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } }; scrollHandler = () => { - if (this.formattingToolbarState?.show) { - this.formattingToolbarState.referencePos = this.getSelectionBoundingBox(); - this.updateFormattingToolbar(); + if (this.state?.show) { + this.state.referencePos = this.getSelectionBoundingBox(); + this.emitUpdate(); } }; @@ -152,24 +145,24 @@ export class FormattingToolbarView { !this.preventShow && (shouldShow || this.preventHide) ) { - this.formattingToolbarState = { + this.state = { show: true, referencePos: this.getSelectionBoundingBox(), }; - this.updateFormattingToolbar(); + this.emitUpdate(); return; } // Checks if menu should be hidden. if ( - this.formattingToolbarState?.show && + this.state?.show && !this.preventHide && (!shouldShow || this.preventShow || !this.editor.isEditable) ) { - this.formattingToolbarState.show = false; - this.updateFormattingToolbar(); + this.state.show = false; + this.emitUpdate(); return; } diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts index 30aa623af3..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 { BaseUiElementState } from "../../extensions-shared/BaseUiElementTypes"; import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { EventEmitter } from "../../util/EventEmitter"; -export type HyperlinkToolbarState = BaseUiElementState & { +export type HyperlinkToolbarState = UiElementPosition & { // The hovered hyperlink's URL, and the text it's displayed with in the // editor. url: string; @@ -15,8 +16,8 @@ export type HyperlinkToolbarState = BaseUiElementState & { }; class HyperlinkToolbarView { - private hyperlinkToolbarState?: HyperlinkToolbarState; - public updateHyperlinkToolbar: () => void; + public state?: HyperlinkToolbarState; + public emitUpdate: () => void; menuUpdateTimer: ReturnType | undefined; startMenuUpdateTimer: () => void; @@ -34,16 +35,14 @@ class HyperlinkToolbarView { constructor( private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, - updateHyperlinkToolbar: ( - hyperlinkToolbarState: HyperlinkToolbarState - ) => void + emitUpdate: (state: HyperlinkToolbarState) => void ) { - this.updateHyperlinkToolbar = () => { - if (!this.hyperlinkToolbarState) { + this.emitUpdate = () => { + if (!this.state) { throw new Error("Attempting to update uninitialized hyperlink toolbar"); } - updateHyperlinkToolbar(this.hyperlinkToolbarState); + emitUpdate(this.state); }; this.startMenuUpdateTimer = () => { @@ -124,22 +123,22 @@ class HyperlinkToolbarView { editorWrapper.contains(event.target as Node) ) ) { - if (this.hyperlinkToolbarState?.show) { - this.hyperlinkToolbarState.show = false; - this.updateHyperlinkToolbar(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } } }; scrollHandler = () => { if (this.hyperlinkMark !== undefined) { - if (this.hyperlinkToolbarState?.show) { - this.hyperlinkToolbarState.referencePos = posToDOMRect( + if (this.state?.show) { + this.state.referencePos = posToDOMRect( this.pmView, this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to ); - this.updateHyperlinkToolbar(); + this.emitUpdate(); } } }; @@ -158,9 +157,9 @@ class HyperlinkToolbarView { this.pmView.dispatch(tr); this.pmView.focus(); - if (this.hyperlinkToolbarState?.show) { - this.hyperlinkToolbarState.show = false; - this.updateHyperlinkToolbar(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } } @@ -176,9 +175,9 @@ class HyperlinkToolbarView { ); this.pmView.focus(); - if (this.hyperlinkToolbarState?.show) { - this.hyperlinkToolbarState.show = false; - this.updateHyperlinkToolbar(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } } @@ -232,7 +231,7 @@ class HyperlinkToolbarView { } if (this.hyperlinkMark && this.editor.isEditable) { - this.hyperlinkToolbarState = { + this.state = { show: true, referencePos: posToDOMRect( this.pmView, @@ -245,19 +244,19 @@ class HyperlinkToolbarView { this.hyperlinkMarkRange!.to ), }; - this.updateHyperlinkToolbar(); + this.emitUpdate(); return; } // Hides menu. if ( - this.hyperlinkToolbarState?.show && + this.state?.show && prevHyperlinkMark && (!this.hyperlinkMark || !this.editor.isEditable) ) { - this.hyperlinkToolbarState.show = false; - this.updateHyperlinkToolbar(); + this.state.show = false; + this.emitUpdate(); return; } diff --git a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts index 98fe041755..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, @@ -9,17 +8,14 @@ import { SpecificBlock, StyleSchema, } from "../../schema"; -import { - BaseUiElementCallbacks, - BaseUiElementState, -} from "../../extensions-shared/BaseUiElementTypes"; -export type ImageToolbarCallbacks = BaseUiElementCallbacks; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; +import { EventEmitter } from "../../util/EventEmitter"; export type ImageToolbarState< B extends BlockSchema, I extends InlineContentSchema, - S extends StyleSchema = StyleSchema -> = BaseUiElementState & { + S extends StyleSchema +> = UiElementPosition & { block: SpecificBlock; }; @@ -28,24 +24,22 @@ export class ImageToolbarView< I extends InlineContentSchema, S extends StyleSchema > { - private imageToolbarState?: ImageToolbarState; - public updateImageToolbar: () => void; + public state?: ImageToolbarState; + public emitUpdate: () => void; public prevWasEditable: boolean | null = null; constructor( private readonly pluginKey: PluginKey, private readonly pmView: EditorView, - updateImageToolbar: ( - imageToolbarState: ImageToolbarState - ) => void + emitUpdate: (state: ImageToolbarState) => void ) { - this.updateImageToolbar = () => { - if (!this.imageToolbarState) { + this.emitUpdate = () => { + if (!this.state) { throw new Error("Attempting to update uninitialized image toolbar"); } - updateImageToolbar(this.imageToolbarState); + emitUpdate(this.state); }; pmView.dom.addEventListener("mousedown", this.mouseDownHandler); @@ -58,17 +52,17 @@ export class ImageToolbarView< } mouseDownHandler = () => { - if (this.imageToolbarState?.show) { - this.imageToolbarState.show = false; - this.updateImageToolbar(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } }; // For dragging the whole editor. dragstartHandler = () => { - if (this.imageToolbarState?.show) { - this.imageToolbarState.show = false; - this.updateImageToolbar(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } }; @@ -88,21 +82,20 @@ export class ImageToolbarView< return; } - if (this.imageToolbarState?.show) { - this.imageToolbarState.show = false; - this.updateImageToolbar(); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); } }; scrollHandler = () => { - if (this.imageToolbarState?.show) { + if (this.state?.show) { const blockElement = document.querySelector( - `[data-node-type="blockContainer"][data-id="${this.imageToolbarState.block.id}"]` + `[data-node-type="blockContainer"][data-id="${this.state.block.id}"]` )!; - this.imageToolbarState.referencePos = - blockElement.getBoundingClientRect(); - this.updateImageToolbar(); + this.state.referencePos = blockElement.getBoundingClientRect(); + this.emitUpdate(); } }; @@ -111,18 +104,18 @@ export class ImageToolbarView< block: SpecificBlock; } = this.pluginKey.getState(view.state); - if (!this.imageToolbarState?.show && pluginState.block) { + if (!this.state?.show && pluginState.block) { const blockElement = document.querySelector( `[data-node-type="blockContainer"][data-id="${pluginState.block.id}"]` )!; - this.imageToolbarState = { + this.state = { show: true, referencePos: blockElement.getBoundingClientRect(), block: pluginState.block, }; - this.updateImageToolbar(); + this.emitUpdate(); return; } @@ -131,10 +124,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.state?.show) { + this.state.show = false; - this.updateImageToolbar(); + this.emitUpdate(); } } } diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index a468dc989a..c86627956d 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -2,19 +2,20 @@ 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 { BaseUiElementState } from "../../extensions-shared/BaseUiElementTypes"; 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"; @@ -24,7 +25,7 @@ export type SideMenuState< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema -> = BaseUiElementState & { +> = UiElementPosition & { // The block that the side menu is attached to. block: Block; }; @@ -255,7 +256,8 @@ export class SideMenuView< S extends StyleSchema > implements PluginView { - private sideMenuState?: SideMenuState; + 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 @@ -273,10 +275,16 @@ export class SideMenuView< constructor( private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, - private readonly updateSideMenu: ( - sideMenuState: SideMenuState - ) => void + emitUpdate: (state: SideMenuState) => void ) { + this.emitUpdate = () => { + if (!this.state) { + throw new Error("Attempting to update uninitialized side menu"); + } + + emitUpdate(this.state); + }; + this.horizontalPosAnchoredAtRoot = true; this.horizontalPosAnchor = ( this.pmView.dom.firstChild! as HTMLElement @@ -369,17 +377,17 @@ export class SideMenuView< }; onKeyDown = (_event: KeyboardEvent) => { - if (this.sideMenuState?.show) { - this.sideMenuState.show = false; - this.updateSideMenu(this.sideMenuState); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); } this.menuFrozen = false; }; onMouseDown = (_event: MouseEvent) => { - if (this.sideMenuState && !this.sideMenuState.show) { - this.sideMenuState.show = true; - this.updateSideMenu(this.sideMenuState); + if (this.state && !this.state.show) { + this.state.show = true; + this.emitUpdate(this.state); } this.menuFrozen = false; }; @@ -421,9 +429,9 @@ export class SideMenuView< editorWrapper.contains(event.target as HTMLElement) ) ) { - if (this.sideMenuState?.show) { - this.sideMenuState.show = false; - this.updateSideMenu(this.sideMenuState); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); } return; @@ -440,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.sideMenuState?.show) { - this.sideMenuState.show = false; - this.updateSideMenu(this.sideMenuState); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); } return; @@ -450,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.sideMenuState?.show && + this.state?.show && this.hoveredBlock?.hasAttribute("data-id") && this.hoveredBlock?.getAttribute("data-id") === block.id ) { @@ -470,7 +478,7 @@ export class SideMenuView< if (this.editor.isEditable) { const blockContentBoundingBox = blockContent.getBoundingClientRect(); - this.sideMenuState = { + this.state = { show: true, referencePos: new DOMRect( this.horizontalPosAnchoredAtRoot @@ -485,16 +493,16 @@ export class SideMenuView< )!, }; - this.updateSideMenu(this.sideMenuState); + this.emitUpdate(this.state); } }; onScroll = () => { - if (this.sideMenuState?.show) { + if (this.state?.show) { const blockContent = this.hoveredBlock!.firstChild as HTMLElement; const blockContentBoundingBox = blockContent.getBoundingClientRect(); - this.sideMenuState.referencePos = new DOMRect( + this.state.referencePos = new DOMRect( this.horizontalPosAnchoredAtRoot ? this.horizontalPosAnchor : blockContentBoundingBox.x, @@ -502,14 +510,14 @@ export class SideMenuView< blockContentBoundingBox.width, blockContentBoundingBox.height ); - this.updateSideMenu(this.sideMenuState); + this.emitUpdate(this.state); } }; destroy() { - if (this.sideMenuState?.show) { - this.sideMenuState.show = false; - this.updateSideMenu(this.sideMenuState); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); } document.body.removeEventListener("mousemove", this.onMouseMove); document.body.removeEventListener("dragover", this.onDragOver); @@ -521,9 +529,9 @@ export class SideMenuView< } addBlock() { - if (this.sideMenuState?.show) { - this.sideMenuState.show = false; - this.updateSideMenu(this.sideMenuState); + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); } this.menuFrozen = true; @@ -585,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) { @@ -593,14 +601,10 @@ export class SideMenuProsemirrorPlugin< this.plugin = new Plugin({ key: sideMenuPluginKey, view: (editorView) => { - this.sideMenuView = new SideMenuView( - editor, - editorView, - (sideMenuState) => { - this.emit("update", sideMenuState); - } - ); - return this.sideMenuView; + this.view = new SideMenuView(editor, editorView, (state) => { + this.emit("update", state); + }); + return this.view; }, }); } @@ -613,7 +617,7 @@ export class SideMenuProsemirrorPlugin< * 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. @@ -622,7 +626,7 @@ export class SideMenuProsemirrorPlugin< dataTransfer: DataTransfer | null; clientY: number; }) => { - this.sideMenuView!.isDragging = true; + this.view!.isDragging = true; dragStart(event, this.editor); }; @@ -635,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 08df513129..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 { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { EventEmitter } from "../../util/EventEmitter"; -import { BaseUiElementState } from "../../extensions-shared/BaseUiElementTypes"; -// TODO: clean file + const findBlock = findParentNode((node) => node.type.name === "blockContainer"); -export type SuggestionsMenuState = BaseUiElementState & { +export type SuggestionMenuState = UiElementPosition & { query: string; }; @@ -17,39 +18,35 @@ class SuggestionMenuView< I extends InlineContentSchema, S extends StyleSchema > { - private suggestionsMenuState?: SuggestionsMenuState; - public updateSuggestionsMenu: (triggerCharacter: string) => void; + private state?: SuggestionMenuState; + public emitUpdate: (triggerCharacter: string) => void; pluginState: SuggestionPluginState; constructor( private readonly editor: BlockNoteEditor, - updateSuggestionsMenu: ( - menuName: string, - suggestionsMenuState: SuggestionsMenuState - ) => void + emitUpdate: (menuName: string, state: SuggestionMenuState) => void ) { this.pluginState = undefined; - this.updateSuggestionsMenu = (menuName: string) => { - if (!this.suggestionsMenuState) { + this.emitUpdate = (menuName: string) => { + if (!this.state) { throw new Error("Attempting to update uninitialized suggestions menu"); } - updateSuggestionsMenu(menuName, this.suggestionsMenuState); + emitUpdate(menuName, this.state); }; document.addEventListener("scroll", this.handleScroll); } handleScroll = () => { - if (this.suggestionsMenuState?.show) { + if (this.state?.show) { const decorationNode = document.querySelector( `[data-decoration-id="${this.pluginState!.decorationId}"]` ); - this.suggestionsMenuState.referencePos = - decorationNode!.getBoundingClientRect(); - this.updateSuggestionsMenu(this.pluginState!.triggerCharacter!); + this.state.referencePos = decorationNode!.getBoundingClientRect(); + this.emitUpdate(this.pluginState!.triggerCharacter!); } }; @@ -73,8 +70,8 @@ class SuggestionMenuView< this.pluginState = stopped ? prev : next; if (stopped || !this.editor.isEditable) { - this.suggestionsMenuState!.show = false; - this.updateSuggestionsMenu(this.pluginState!.triggerCharacter); + this.state!.show = false; + this.emitUpdate(this.pluginState!.triggerCharacter); return; } @@ -84,13 +81,13 @@ class SuggestionMenuView< ); if (this.editor.isEditable) { - this.suggestionsMenuState = { + this.state = { show: true, referencePos: decorationNode!.getBoundingClientRect(), query: this.pluginState!.query, }; - this.updateSuggestionsMenu(this.pluginState!.triggerCharacter!); + this.emitUpdate(this.pluginState!.triggerCharacter!); } } @@ -168,8 +165,8 @@ export class SuggestionMenuProseMirrorPlugin< view: () => { this.view = new SuggestionMenuView( editor, - (triggerCharacter, suggestionsMenuState) => { - this.emit(`update ${triggerCharacter}`, suggestionsMenuState); + (triggerCharacter, state) => { + this.emit(`update ${triggerCharacter}`, state); } ); return this.view; @@ -322,7 +319,7 @@ export class SuggestionMenuProseMirrorPlugin< public onUpdate( triggerCharacter: string, - callback: (state: SuggestionsMenuState) => void + callback: (state: SuggestionMenuState) => void ) { if (!this.triggerCharacters.includes(triggerCharacter)) { this.addTriggerCharacter(triggerCharacter); @@ -335,6 +332,13 @@ export class SuggestionMenuProseMirrorPlugin< 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/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/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..6a3df0e517 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..794e14830d 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultFormattingToolbar.tsx @@ -1,23 +1,32 @@ -import { BlockSchema } from "@blocknote/core"; +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, +} 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 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/FormattingToolbar/FormattingToolbarPositioner.tsx b/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx similarity index 57% rename from packages/react/src/components/FormattingToolbar/FormattingToolbarPositioner.tsx rename to packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx index 24f52f1937..e673f50375 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbarPositioner.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx @@ -4,16 +4,16 @@ import { DefaultBlockSchema, DefaultProps, } from "@blocknote/core"; -import { - flip, - offset, - useFloating, - useTransitionStyles, -} from "@floating-ui/react"; -import { FC, useEffect, useRef, useState } from "react"; +import { flip, offset } from "@floating-ui/react"; +import { FC, useState } from "react"; +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; import { useEditorChange } from "../../hooks/useEditorChange"; -import { DefaultFormattingToolbar } from "./DefaultFormattingToolbar"; +import { + DefaultFormattingToolbar, + FormattingToolbarProps, +} from "./DefaultFormattingToolbar"; const textAlignmentToPlacement = ( textAlignment: DefaultProps["textAlignment"] @@ -30,19 +30,12 @@ const textAlignmentToPlacement = ( } }; -export type FormattingToolbarProps< - BSchema extends BlockSchema = DefaultBlockSchema -> = { - editor: BlockNoteEditor; -}; - -export const FormattingToolbarPositioner = < +export const DefaultPositionedFormattingToolbar = < BSchema extends BlockSchema = DefaultBlockSchema >(props: { editor: BlockNoteEditor; formattingToolbar?: FC>; }) => { - const [show, setShow] = useState(false); const [placement, setPlacement] = useState<"top-start" | "top" | "top-end">( () => { const block = props.editor.getTextCursorPosition().block; @@ -57,26 +50,6 @@ export const FormattingToolbarPositioner = < } ); - const referencePos = useRef(); - - const { refs, update, context, floatingStyles } = useFloating({ - open: show, - placement, - middleware: [offset(10), flip()], - }); - - const { isMounted, styles } = useTransitionStyles(context); - - useEffect(() => { - return props.editor.formattingToolbar.onUpdate((state) => { - setShow(state.show); - - referencePos.current = state.referencePos; - - update(); - }); - }, [props.editor, update]); - useEditorChange(props.editor, () => { const block = props.editor.getTextCursorPosition().block; @@ -91,22 +64,27 @@ export const FormattingToolbarPositioner = < } }); - useEffect(() => { - refs.setReference({ - getBoundingClientRect: () => referencePos.current!, - }); - }, [refs]); - - const FormattingToolbar = props.formattingToolbar || DefaultFormattingToolbar; + const state = useUIPluginState( + props.editor.formattingToolbar.onUpdate.bind(props.editor.formattingToolbar) + ); + const { isMounted, ref, style } = useUiElementPositioning( + state?.show || false, + state?.referencePos || null, + 3000, + { + placement, + middleware: [offset(10), flip()], + } + ); - if (!isMounted) { + if (!isMounted || !state) { return null; } + const FormattingToolbar = props.formattingToolbar || DefaultFormattingToolbar; + return ( -
+
); diff --git a/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx b/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx index 4e9e72f1ce..1bbbc2334c 100644 --- a/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx +++ b/packages/react/src/components/HyperlinkToolbar/DefaultHyperlinkToolbar.tsx @@ -1,29 +1,43 @@ -import { BlockSchema, InlineContentSchema } from "@blocknote/core"; +import { + BlockNoteEditor, + HyperlinkToolbarState, + UiElementPosition, +} from "@blocknote/core"; import { useRef, useState } from "react"; import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; -import { StyleSchema } from "@blocknote/core"; 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 -) => { +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); + const { + text, + url, + deleteHyperlink, + editHyperlink, + startHideTimer, + stopHideTimer, + } = props; + if (isEditing) { return ( setTimeout(() => { @@ -39,9 +53,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..63f62c782e --- /dev/null +++ b/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx @@ -0,0 +1,61 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { flip, offset } from "@floating-ui/react"; +import { FC } from "react"; + +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; +import { + DefaultHyperlinkToolbar, + HyperlinkToolbarProps, +} from "./DefaultHyperlinkToolbar"; + +export const DefaultPositionedHyperlinkToolbar = < + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(props: { + editor: BlockNoteEditor; + hyperlinkToolbar?: FC; +}) => { + const callbacks = { + deleteHyperlink: props.editor.hyperlinkToolbar.deleteHyperlink, + editHyperlink: props.editor.hyperlinkToolbar.editHyperlink, + startHideTimer: props.editor.hyperlinkToolbar.startHideTimer, + stopHideTimer: props.editor.hyperlinkToolbar.stopHideTimer, + }; + + const state = useUIPluginState( + props.editor.hyperlinkToolbar.onUpdate.bind(props.editor.hyperlinkToolbar) + ); + const { isMounted, ref, style } = useUiElementPositioning( + 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/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/ImageToolbar/DefaultImageToolbar.tsx b/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx index d02e1a2579..b8444e5f7c 100644 --- a/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx +++ b/packages/react/src/components/ImageToolbar/DefaultImageToolbar.tsx @@ -1,5 +1,11 @@ -import { BlockSchema, PartialBlock } from "@blocknote/core"; - +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + ImageToolbarState, + PartialBlock, + UiElementPosition, +} from "@blocknote/core"; import { Button, FileInput, @@ -15,11 +21,17 @@ import { useEffect, useState, } from "react"; + import { Toolbar } from "../../components-shared/Toolbar/Toolbar"; -import type { ImageToolbarProps } from "./ImageToolbarPositioner"; + +export type ImageToolbarProps< + BSchema extends BlockSchema = DefaultBlockSchema +> = { + editor: BlockNoteEditor; +} & Omit, keyof UiElementPosition>; export const DefaultImageToolbar = ( - props: ImageToolbarProps + props: ImageToolbarProps ) => { const [openTab, setOpenTab] = useState<"upload" | "embed">( props.editor.uploadFile !== undefined ? "upload" : "embed" diff --git a/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx b/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx new file mode 100644 index 0000000000..3e8d757dbd --- /dev/null +++ b/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx @@ -0,0 +1,45 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, +} from "@blocknote/core"; +import { FC } from "react"; +import { flip, offset } from "@floating-ui/react"; + +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; +import { DefaultImageToolbar, ImageToolbarProps } from "./DefaultImageToolbar"; + +export const DefaultPositionedImageToolbar = < + BSchema extends BlockSchema = DefaultBlockSchema +>(props: { + editor: BlockNoteEditor; + imageToolbar?: FC>; +}) => { + const state = useUIPluginState( + props.editor.imageToolbar.onUpdate.bind(props.editor.imageToolbar) + ); + const { isMounted, ref, style } = useUiElementPositioning( + state?.show || false, + state?.referencePos || null, + 5000, + { + placement: "bottom", + middleware: [offset(10), flip()], + } + ); + + if (!isMounted || !state) { + return null; + } + + const { show, referencePos, ...data } = state; + + 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/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..903ddb3e0d --- /dev/null +++ b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx @@ -0,0 +1,57 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { FC } from "react"; + +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; +import { DefaultSideMenu, SideMenuProps } from "./DefaultSideMenu"; + +export const DefaultPositionedSideMenu = < + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>(props: { + editor: BlockNoteEditor; + sideMenu?: FC>; +}) => { + 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, + }; + + const state = useUIPluginState( + props.editor.sideMenu.onUpdate.bind(props.editor.sideMenu) + ); + const { isMounted, ref, style } = useUiElementPositioning( + 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 9bdf66991e..63db4a328d 100644 --- a/packages/react/src/components/SideMenu/DefaultSideMenu.tsx +++ b/packages/react/src/components/SideMenu/DefaultSideMenu.tsx @@ -1,20 +1,50 @@ -import { BlockSchema, InlineContentSchema } from "@blocknote/core"; - -import { StyleSchema } from "@blocknote/core"; +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + SideMenuState, + StyleSchema, + UiElementPosition, +} from "@blocknote/core"; import { AddBlockButton } from "./DefaultButtons/AddBlockButton"; import { DragHandle } from "./DefaultButtons/DragHandle"; import { SideMenu } from "./SideMenu"; -import type { SideMenuProps } from "./SideMenuPositioner"; +import { FC } from "react"; +import type { DragHandleMenuProps } from "./DragHandleMenu/DragHandleMenu"; + +export type SideMenuProps< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +> = { + editor: BlockNoteEditor; + dragHandleMenu?: FC>; +} & Omit, keyof UiElementPosition> & + Pick< + BlockNoteEditor["sideMenu"], + | "addBlock" + | "blockDragStart" + | "blockDragEnd" + | "freezeMenu" + | "unfreezeMenu" + >; 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 -) => ( - - - - -); +) => { + const { addBlock, ...rest } = props; + + 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/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx new file mode 100644 index 0000000000..a00f2d6d6b --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx @@ -0,0 +1,89 @@ +import { + BlockNoteEditor, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, + SuggestionMenuState, +} from "@blocknote/core"; +import { flip, offset, size } from "@floating-ui/react"; +import { FC } from "react"; + +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; +import { DefaultSuggestionMenu } from "./DefaultSuggestionMenu"; +import { MantineSuggestionMenuProps } from "./MantineDefaults/MantineSuggestionMenu"; +import { MantineSuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; + +export function DefaultPositionedSuggestionMenu< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema, + Item extends { + name: string; + execute: () => void; + } = MantineSuggestionMenuItemProps +>(props: { + editor: BlockNoteEditor; + triggerCharacter?: string; + getItems?: ( + query: string, + closeMenu: () => void, + clearQuery: () => void + ) => Promise; + suggestionMenuComponent?: FC>; +}) { + const callbacks = { + closeMenu: props.editor.suggestionMenus.closeMenu, + clearQuery: props.editor.suggestionMenus.clearQuery, + }; + + const state = useUIPluginState( + (callback: (state: SuggestionMenuState) => void) => + props.editor.suggestionMenus.onUpdate.bind(props.editor.suggestionMenus)( + props.triggerCharacter || "/", + callback + ) + ); + const { isMounted, ref, style } = useUiElementPositioning( + 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 || !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 2212b81bd1..4907274e49 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx @@ -2,99 +2,62 @@ import { FC, useMemo } from "react"; import { BlockNoteEditor, BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, InlineContentSchema, StyleSchema, + SuggestionMenuState, + UiElementPosition, } from "@blocknote/core"; - -import { SuggestionMenuItemProps } from "./MantineSuggestionMenuItem"; 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"; + MantineSuggestionMenuProps, +} from "./MantineDefaults/MantineSuggestionMenu"; +import { MantineSuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; -export function DefaultPositionedSuggestionMenu< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, +export type SuggestionMenuProps< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema, Item extends { name: string; execute: () => void; - } = SuggestionMenuItemProps ->(props: { + } = MantineSuggestionMenuItemProps +> = { editor: BlockNoteEditor; - triggerCharacter?: string; getItems?: ( query: string, 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>; +} & Omit & + Pick< + BlockNoteEditor["suggestionMenus"], + "closeMenu" | "clearQuery" + >; - 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, - S extends StyleSchema, + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema, Item extends { name: string; execute: () => void; - } = SuggestionMenuItemProps ->(props: { - editor: BlockNoteEditor; - query: string; - closeMenu: () => void; - clearQuery: () => void; - getItems?: ( - query: string, - closeMenu: () => void, - clearQuery: () => void - ) => Promise; - suggestionMenuComponent?: FC>; -}) { + } = MantineSuggestionMenuItemProps +>(props: SuggestionMenuProps) { const { editor, + getItems, + suggestionMenuComponent, query, closeMenu, clearQuery, - getItems, - suggestionMenuComponent, } = props; const getItemsForLoading = useMemo<(query: string) => Promise>( @@ -120,11 +83,11 @@ export function DefaultSuggestionMenu< closeMenu ); - const SuggestionMenu: FC> = + const SuggestionMenuComponent: FC> = suggestionMenuComponent || MantineSuggestionMenu; return ( - = { +export type MantineSuggestionMenuProps = { items: T[]; loadingState: "loading-initial" | "loading" | "loaded"; selectedIndex: number; }; export function MantineSuggestionMenu( - props: SuggestionMenuProps + props: MantineSuggestionMenuProps ) { 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 94% rename from packages/react/src/components/SuggestionMenu/MantineSuggestionMenuItem.tsx rename to packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenuItem.tsx index df45952187..68cc266cf2 100644 --- a/packages/react/src/components/SuggestionMenu/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 3471ad79f8..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 "./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 @@ -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, @@ -101,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: () => { @@ -253,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 d8cae06427..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 "../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 edef4b737c..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 "../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/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..e7882af7c1 --- /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 { useUIPluginState } from "../../hooks/useUIPluginState"; +import { useTableHandlesPositioning } from "./hooks/useTableHandlesPositioning"; +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 = useUIPluginState( + props.editor.tableHandles!.onUpdate.bind(props.editor.tableHandles) + ); + const { rowHandle, colHandle } = useTableHandlesPositioning( + 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/useTableHandlesPositioning.ts b/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts new file mode 100644 index 0000000000..4f1406a81e --- /dev/null +++ b/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts @@ -0,0 +1,144 @@ +import { offset, useFloating, useTransitionStyles } from "@floating-ui/react"; +import { useEffect, useMemo } from "react"; + +import { UiComponentPosition } from "../../../components-shared/UiComponentTypes"; + +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 + ); + } + + return new DOMRect( + referencePosTable!.x, + referencePosCell!.y, + referencePosTable!.width, + referencePosCell!.height + ); +} + +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 + ); + } + + 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 { + const { refs, update, context, floatingStyles } = useFloating({ + open: show, + placement: orientation === "row" ? "left" : "top", + middleware: [offset(orientation === "row" ? -10 : -12)], + }); + + const { isMounted, styles } = useTransitionStyles(context); + + useEffect(() => { + update(); + }, [referencePosCell, referencePosTable, update]); + + useEffect(() => { + // TODO: Maybe throw error instead if null + if (referencePosCell === null || referencePosTable === null) { + return; + } + + refs.setReference({ + getBoundingClientRect: () => { + const fn = + orientation === "row" + ? getBoundingClientRectRow + : getBoundingClientRectCol; + return fn(referencePosCell, referencePosTable, draggingState); + }, + }); + }, [draggingState, orientation, referencePosCell, referencePosTable, refs]); + + return useMemo( + () => ({ + isMounted: isMounted, + ref: refs.setFloating, + style: { + display: "flex", + ...styles, + ...floatingStyles, + zIndex: 10000, + }, + }), + [floatingStyles, isMounted, refs.setFloating, styles] + ); +} + +export function useTableHandlesPositioning( + 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 65f8d1a9e9..7a7198a8ac 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -1,9 +1,9 @@ -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 { TableHandlesPositioner } from "../components/TableHandles/TableHandlePositioner"; +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 { DefaultPositionedTableHandles } from "../components/TableHandles/DefaultPositionedTableHandles"; import { BlockNoteEditor, BlockSchema, @@ -27,20 +27,22 @@ 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/hooks/useSuggestionMenu.ts b/packages/react/src/hooks/useSuggestionMenu.ts deleted file mode 100644 index 23eb0bd9ca..0000000000 --- a/packages/react/src/hooks/useSuggestionMenu.ts +++ /dev/null @@ -1,87 +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"; - -// TODO: maybe separate the positioning part (floating ui) -export function useSuggestionMenu< - 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(); - - 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.onUpdate( - triggerCharacter, - (suggestionsMenuState) => { - setShow(suggestionsMenuState.show); - setQuery(suggestionsMenuState.query); - - referencePos.current = suggestionsMenuState.referencePos; - - update(); - } - ); - }, [editor.suggestionMenus, triggerCharacter, show, update]); - - useEffect(() => { - refs.setReference({ - getBoundingClientRect: () => referencePos.current!, - }); - }, [refs]); - - return { - isMounted: isMounted, - suggestionMenuProps: { - query, - closeMenu: editor.suggestionMenus.closeMenu, - clearQuery: editor.suggestionMenus.clearQuery, - }, - positionerProps: { - ref: refs.setFloating, - styles: { - display: "flex", - ...styles, - ...floatingStyles, - zIndex: 2000, - }, - }, - }; -} diff --git a/packages/react/src/hooks/useUIPluginState.ts b/packages/react/src/hooks/useUIPluginState.ts new file mode 100644 index 0000000000..c49ae72ea1 --- /dev/null +++ b/packages/react/src/hooks/useUIPluginState.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; + +export function useUIPluginState( + 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/useUiElementPositioning.ts b/packages/react/src/hooks/useUiElementPositioning.ts new file mode 100644 index 0000000000..d37950ed2d --- /dev/null +++ b/packages/react/src/hooks/useUiElementPositioning.ts @@ -0,0 +1,51 @@ +import { + useFloating, + UseFloatingOptions, + useTransitionStyles, +} from "@floating-ui/react"; +import { useEffect, useMemo } from "react"; + +import { UiComponentPosition } from "../components-shared/UiComponentTypes"; + +export function useUiElementPositioning( + 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) { + return; + } + + refs.setReference({ + getBoundingClientRect: () => referencePos, + }); + }, [referencePos, refs]); + + return useMemo( + () => ({ + isMounted, + ref: refs.setFloating, + style: { + display: "flex", + ...styles, + ...floatingStyles, + zIndex: zIndex, + }, + }), + [floatingStyles, isMounted, refs.setFloating, styles, zIndex] + ); +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index d0e698c136..249fa885db 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -13,16 +13,17 @@ 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/DefaultHyperlinkToolbar"; +export * from "./components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar"; 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,19 +31,21 @@ 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/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/DefaultImageToolbar"; -export * from "./components/ImageToolbar/ImageToolbarPositioner"; +export * from "./components/ImageToolbar/DefaultPositionedImageToolbar"; +export * from "./components/TableHandles/hooks/useTableHandlesPositioning"; 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"; @@ -55,7 +58,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"; 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);