diff --git a/examples/editor/examples/basic/App.tsx b/examples/editor/examples/basic/App.tsx index bfce7bf560..2a1c19e5d7 100644 --- a/examples/editor/examples/basic/App.tsx +++ b/examples/editor/examples/basic/App.tsx @@ -1,10 +1,6 @@ import { - BlockNoteEditor, - DefaultBlockSchema, - defaultInlineContentSchema, defaultInlineContentSpecs, - DefaultStyleSchema, - InlineContentSchema, + filterSuggestionItems, InlineContentSpecs, uploadToTmpFilesDotOrg_DEV_ONLY, } from "@blocknote/core"; @@ -13,7 +9,6 @@ import { BlockNoteView, createReactInlineContentSpec, DefaultPositionedSuggestionMenu, - MantineSuggestionMenuItemProps, useBlockNote, } from "@blocknote/react"; import "@blocknote/react/style.css"; @@ -43,46 +38,16 @@ const customInlineContentSpecs = { ...defaultInlineContentSpecs, mention: MentionInlineContent, } satisfies InlineContentSpecs; -const customInlineContentSchema = { - ...defaultInlineContentSchema, - mention: MentionInlineContent.config, -} satisfies InlineContentSchema; -async function getMentionMenuItems( - editor: BlockNoteEditor< - DefaultBlockSchema, - typeof customInlineContentSchema, - DefaultStyleSchema - >, - query: string, - closeMenu: () => void, - clearQuery: () => void -): Promise { +async function getMentionMenuItems(query: string) { const users = ["Steve", "Bob", "Joe", "Mike"]; - const items: MantineSuggestionMenuItemProps[] = users.map((user) => ({ - name: user, - execute: () => { - closeMenu(); - clearQuery(); + const items = users.map((user) => ({ + title: user, - editor._tiptapEditor.commands.insertContent({ - type: "mention", - attrs: { - user: user, - }, - }); - }, aliases: [] as string[], })); - return items.filter( - ({ name, aliases }) => - name.toLowerCase().startsWith(query.toLowerCase()) || - (aliases && - aliases.filter((alias) => - alias.toLowerCase().startsWith(query.toLowerCase()) - ).length !== 0) - ); + return filterSuggestionItems(items, query); } export function App() { @@ -107,9 +72,15 @@ export function App() { - getMentionMenuItems(editor, query, closeMenu, clearQuery) - } + getItems={async (query) => getMentionMenuItems(query)} + onItemClick={(item) => { + editor._tiptapEditor.commands.insertContent({ + type: "mention", + attrs: { + user: item.title, + }, + }); + }} /> ); diff --git a/packages/react/src/components/SuggestionMenu/defaultGetItems.tsx b/packages/core/src/extensions/SuggestionMenu/defaultSlashMenuItems.ts similarity index 62% rename from packages/react/src/components/SuggestionMenu/defaultGetItems.tsx rename to packages/core/src/extensions/SuggestionMenu/defaultSlashMenuItems.ts index 3b5516a9b0..c3ced59839 100644 --- a/packages/react/src/components/SuggestionMenu/defaultGetItems.tsx +++ b/packages/core/src/extensions/SuggestionMenu/defaultSlashMenuItems.ts @@ -1,29 +1,15 @@ import { Block, - BlockNoteEditor, BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - formatKeyboardShortcut, - imageToolbarPluginKey, InlineContentSchema, isStyledTextInlineContent, PartialBlock, StyleSchema, -} from "@blocknote/core"; -import { - RiH1, - RiH2, - RiH3, - RiImage2Fill, - RiListOrdered, - RiListUnordered, - RiTable2, - RiText, -} from "react-icons/ri"; - -import { MantineSuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; +} from "../../schema"; +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; +import { DefaultBlockSchema } from "../../blocks/defaultBlocks"; +import { formatKeyboardShortcut } from "../../util/browser"; +import { imageToolbarPluginKey } from "../ImageToolbar/ImageToolbarPlugin"; // 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,117 +74,89 @@ export function insertOrUpdateBlock< return insertedBlock; } -// TODO: Probably want an easier way of customizing the items list. -export async function defaultGetItems< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema ->( - editor: BlockNoteEditor, - query: string, - closeMenu: () => void, - clearQuery: () => void -): Promise { - const items: MantineSuggestionMenuItemProps[] = [ +export function getDefaultSlashMenuItems() { + return [ { - name: "Heading 1", - execute: () => { - closeMenu(); - clearQuery(); - + title: "Heading 1", + // Unfortunately, we can't use a more specific BlockNoteEditor type here, + // Typescript seems to get in the way there + // This means that we don't have type checking for calling insertOrUpdateBlock etc. :( + onItemClick: (editor: BlockNoteEditor) => { insertOrUpdateBlock(editor, { type: "heading", props: { level: 1 }, - } as PartialBlock); + } satisfies PartialBlock); }, subtext: "Used for a top-level heading", - icon: , badge: formatKeyboardShortcut("Mod-Alt-1"), aliases: ["h", "heading1", "h1"], group: "Headings", }, { - name: "Heading 2", - execute: () => { - closeMenu(); - clearQuery(); - + title: "Heading 2", + onItemClick: (editor: BlockNoteEditor) => { insertOrUpdateBlock(editor, { type: "heading", props: { level: 2 }, - } as PartialBlock); + } satisfies PartialBlock); }, subtext: "Used for key sections", - icon: , badge: formatKeyboardShortcut("Mod-Alt-2"), aliases: ["h2", "heading2", "subheading"], group: "Headings", }, { - name: "Heading 3", - execute: () => { - closeMenu(); - clearQuery(); - + title: "Heading 3", + onItemClick: (editor: BlockNoteEditor) => { insertOrUpdateBlock(editor, { type: "heading", props: { level: 3 }, - } as PartialBlock); + } satisfies PartialBlock); }, subtext: "Used for subsections and group headings", - icon: , badge: formatKeyboardShortcut("Mod-Alt-3"), aliases: ["h3", "heading3", "subheading"], group: "Headings", }, { - name: "Numbered List", - execute: () => { - closeMenu(); - clearQuery(); - - insertOrUpdateBlock(editor, { type: "numberedListItem" }); + title: "Numbered List", + onItemClick: (editor: BlockNoteEditor) => { + insertOrUpdateBlock(editor, { + type: "numberedListItem", + } satisfies PartialBlock); }, subtext: "Used to display a numbered list", - icon: , badge: formatKeyboardShortcut("Mod-Shift-7"), aliases: ["ol", "li", "list", "numberedlist", "numbered list"], group: "Basic blocks", }, { - name: "Bullet List", - execute: () => { - closeMenu(); - clearQuery(); - - insertOrUpdateBlock(editor, { type: "bulletListItem" }); + title: "Bullet List", + onItemClick: (editor: BlockNoteEditor) => { + insertOrUpdateBlock(editor, { + type: "bulletListItem", + } satisfies PartialBlock); }, subtext: "Used to display an unordered list", - icon: , badge: formatKeyboardShortcut("Mod-Shift-8"), aliases: ["ul", "li", "list", "bulletlist", "bullet list"], group: "Basic blocks", }, { - name: "Paragraph", - execute: () => { - closeMenu(); - clearQuery(); - - insertOrUpdateBlock(editor, { type: "paragraph" }); + title: "Paragraph", + onItemClick: (editor: BlockNoteEditor) => { + insertOrUpdateBlock(editor, { + type: "paragraph", + } satisfies PartialBlock); }, subtext: "Used for the body of your document", - icon: , badge: formatKeyboardShortcut("Mod-Alt-0"), aliases: ["p", "paragraph"], group: "Basic blocks", }, { - name: "Table", - execute: () => { - closeMenu(); - clearQuery(); - + title: "Table", + onItemClick: (editor: BlockNoteEditor) => { insertOrUpdateBlock(editor, { type: "table", content: { @@ -212,22 +170,19 @@ export async function defaultGetItems< }, ], }, - } as PartialBlock); + } satisfies PartialBlock); }, subtext: "Used for for tables", - icon: , aliases: ["table"], group: "Advanced", + badge: undefined, }, { - name: "Image", - execute: () => { - closeMenu(); - clearQuery(); - + title: "Image", + onItemClick: (editor: BlockNoteEditor) => { const insertedBlock = insertOrUpdateBlock(editor, { type: "image", - }); + } satisfies PartialBlock); // Immediately open the image toolbar editor._tiptapEditor.view.dispatch( @@ -237,7 +192,6 @@ export async function defaultGetItems< ); }, subtext: "Insert an image", - icon: , aliases: [ "image", "imageUpload", @@ -250,29 +204,16 @@ export async function defaultGetItems< "dropbox", ], group: "Media", - } satisfies MantineSuggestionMenuItemProps, - ]; - - // For testing async - // return new Promise((resolve) => { - // setTimeout(() => { - // console.log("return items"); - // resolve( - // items.filter( - // ({ name, aliases }) => - // name.toLowerCase().startsWith(query.toLowerCase()) || - // (aliases && - // aliases.filter((alias) => - // alias.toLowerCase().startsWith(query.toLowerCase()) - // ).length !== 0) - // ) - // ); - // }, 1000); - // }); + }, + ] as const; +} +export function filterSuggestionItems< + T extends { title: string; aliases?: readonly string[] } +>(items: T[], query: string) { return items.filter( - ({ name, aliases }) => - name.toLowerCase().startsWith(query.toLowerCase()) || + ({ title, aliases }) => + title.toLowerCase().startsWith(query.toLowerCase()) || (aliases && aliases.filter((alias) => alias.toLowerCase().startsWith(query.toLowerCase()) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3a795c5157..1c6603c158 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,6 +10,7 @@ export * from "./editor/selectionTypes"; export * from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; export * from "./extensions/HyperlinkToolbar/HyperlinkToolbarPlugin"; export * from "./extensions/ImageToolbar/ImageToolbarPlugin"; +export * from "./extensions/SuggestionMenu/defaultSlashMenuItems"; export * from "./extensions/SuggestionMenu/SuggestionPlugin"; export * from "./extensions/SideMenu/SideMenuPlugin"; export * from "./extensions/TableHandles/TableHandlesPlugin"; diff --git a/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx b/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx index e673f50375..464b0ddf44 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultPositionedFormattingToolbar.tsx @@ -8,7 +8,7 @@ import { flip, offset } from "@floating-ui/react"; import { FC, useState } from "react"; import { useUIPluginState } from "../../hooks/useUIPluginState"; -import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; +import { useUIElementPositioning } from "../../hooks/useUIElementPositioning"; import { useEditorChange } from "../../hooks/useEditorChange"; import { DefaultFormattingToolbar, @@ -67,7 +67,7 @@ export const DefaultPositionedFormattingToolbar = < const state = useUIPluginState( props.editor.formattingToolbar.onUpdate.bind(props.editor.formattingToolbar) ); - const { isMounted, ref, style } = useUiElementPositioning( + const { isMounted, ref, style } = useUIElementPositioning( state?.show || false, state?.referencePos || null, 3000, diff --git a/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx b/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx index 63f62c782e..b6b59a7db4 100644 --- a/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx +++ b/packages/react/src/components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar.tsx @@ -11,7 +11,7 @@ import { flip, offset } from "@floating-ui/react"; import { FC } from "react"; import { useUIPluginState } from "../../hooks/useUIPluginState"; -import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; +import { useUIElementPositioning } from "../../hooks/useUIElementPositioning"; import { DefaultHyperlinkToolbar, HyperlinkToolbarProps, @@ -35,7 +35,7 @@ export const DefaultPositionedHyperlinkToolbar = < const state = useUIPluginState( props.editor.hyperlinkToolbar.onUpdate.bind(props.editor.hyperlinkToolbar) ); - const { isMounted, ref, style } = useUiElementPositioning( + const { isMounted, ref, style } = useUIElementPositioning( state?.show || false, state?.referencePos || null, 4000, diff --git a/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx b/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx index 3e8d757dbd..6fc7455138 100644 --- a/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx +++ b/packages/react/src/components/ImageToolbar/DefaultPositionedImageToolbar.tsx @@ -7,7 +7,7 @@ import { FC } from "react"; import { flip, offset } from "@floating-ui/react"; import { useUIPluginState } from "../../hooks/useUIPluginState"; -import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; +import { useUIElementPositioning } from "../../hooks/useUIElementPositioning"; import { DefaultImageToolbar, ImageToolbarProps } from "./DefaultImageToolbar"; export const DefaultPositionedImageToolbar = < @@ -19,7 +19,7 @@ export const DefaultPositionedImageToolbar = < const state = useUIPluginState( props.editor.imageToolbar.onUpdate.bind(props.editor.imageToolbar) ); - const { isMounted, ref, style } = useUiElementPositioning( + const { isMounted, ref, style } = useUIElementPositioning( state?.show || false, state?.referencePos || null, 5000, diff --git a/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx index 903ddb3e0d..bdefa9592a 100644 --- a/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx +++ b/packages/react/src/components/SideMenu/DefaultPositionedSideMenu.tsx @@ -10,7 +10,7 @@ import { import { FC } from "react"; import { useUIPluginState } from "../../hooks/useUIPluginState"; -import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; +import { useUIElementPositioning } from "../../hooks/useUIElementPositioning"; import { DefaultSideMenu, SideMenuProps } from "./DefaultSideMenu"; export const DefaultPositionedSideMenu = < @@ -32,7 +32,7 @@ export const DefaultPositionedSideMenu = < const state = useUIPluginState( props.editor.sideMenu.onUpdate.bind(props.editor.sideMenu) ); - const { isMounted, ref, style } = useUiElementPositioning( + const { isMounted, ref, style } = useUIElementPositioning( state?.show || false, state?.referencePos || null, 1000, diff --git a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx index a00f2d6d6b..d4c11ced18 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultPositionedSuggestionMenu.tsx @@ -1,9 +1,6 @@ import { BlockNoteEditor, BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, InlineContentSchema, StyleSchema, SuggestionMenuState, @@ -11,43 +8,65 @@ import { import { flip, offset, size } from "@floating-ui/react"; import { FC } from "react"; +import { useUIElementPositioning } from "../../hooks/useUIElementPositioning"; import { useUIPluginState } from "../../hooks/useUIPluginState"; -import { useUiElementPositioning } from "../../hooks/useUiElementPositioning"; import { DefaultSuggestionMenu } from "./DefaultSuggestionMenu"; -import { MantineSuggestionMenuProps } from "./MantineDefaults/MantineSuggestionMenu"; -import { MantineSuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; +import { MantineSuggestionMenu } from "./mantine/MantineSuggestionMenu"; +import { DefaultSuggestionItem, SuggestionMenuProps } from "./types"; + +type ArrayElement = A extends readonly (infer T)[] ? T : never; + +type ItemType Promise> = + ArrayElement>>; 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>; -}) { + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, + // This is a bit hacky, but only way I found to make types work so the optionality + // of suggestionMenuComponent depends on the return type of getItems + GetItemsType extends (query: string) => Promise +>( + props: { + editor: BlockNoteEditor; + triggerCharacter: string; + getItems: GetItemsType; + onItemClick?: (item: ItemType) => void; + } & (ItemType extends DefaultSuggestionItem + ? { + // can be undefined + suggestionMenuComponent?: FC< + SuggestionMenuProps> + >; + } + : { + // getItems doesn't return DefaultSuggestionItem, so suggestionMenuComponent is required + suggestionMenuComponent: FC< + SuggestionMenuProps> + >; + }) +) { + const { + editor, + triggerCharacter, + onItemClick, + getItems, + suggestionMenuComponent, + } = props; + const callbacks = { - closeMenu: props.editor.suggestionMenus.closeMenu, - clearQuery: props.editor.suggestionMenus.clearQuery, + closeMenu: editor.suggestionMenus.closeMenu, + clearQuery: editor.suggestionMenus.clearQuery, }; const state = useUIPluginState( (callback: (state: SuggestionMenuState) => void) => - props.editor.suggestionMenus.onUpdate.bind(props.editor.suggestionMenus)( - props.triggerCharacter || "/", + props.editor.suggestionMenus.onUpdate.bind(editor.suggestionMenus)( + triggerCharacter, callback ) ); - const { isMounted, ref, style } = useUiElementPositioning( + const { isMounted, ref, style } = useUIElementPositioning( state?.show || false, state?.referencePos || null, 2000, @@ -78,9 +97,12 @@ export function DefaultPositionedSuggestionMenu< return (
diff --git a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.test.tsx b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.test.tsx new file mode 100644 index 0000000000..a797d5fe2f --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.test.tsx @@ -0,0 +1,40 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { it } from "vitest"; +import { DefaultPositionedSuggestionMenu } from "./DefaultPositionedSuggestionMenu"; + +it("has good typing", () => { + // invalid, because DefaultSuggestionItem doesn't have a title property, so the default MantineSuggestionMenu doesn't wrok + let menu = ( + // @ts-expect-error + [{ name: "hello" }]} + editor={undefined as any} + triggerCharacter="/" + /> + ); + + // valid, because getItems returns DefaultSuggestionItem so suggestionMenuComponent is optional + menu = ( + [{ title: "hello" }]} + editor={undefined as any} + triggerCharacter="/" + /> + ); + + // validate type of onItemClick + menu = ( + [{ hello: "hello" }]} + editor={undefined as any} + onItemClick={(item) => { + console.log(item.hello); + }} + triggerCharacter="/" + /> + ); + + // prevent typescript unused error + console.log("menu", menu); +}); diff --git a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx index 4907274e49..4243c1dde5 100644 --- a/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/DefaultSuggestionMenu.tsx @@ -1,78 +1,52 @@ -import { FC, useMemo } from "react"; import { BlockNoteEditor, BlockSchema, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, InlineContentSchema, StyleSchema, - SuggestionMenuState, - UiElementPosition, } from "@blocknote/core"; -import { useLoadSuggestionMenuItems } from "./hooks/useLoadSuggestionMenuItems"; +import { FC, useCallback } from "react"; + import { useCloseSuggestionMenuNoItems } from "./hooks/useCloseSuggestionMenuNoItems"; +import { useLoadSuggestionMenuItems } from "./hooks/useLoadSuggestionMenuItems"; import { useSuggestionMenuKeyboardNavigation } from "./hooks/useSuggestionMenuKeyboardNavigation"; -import { defaultGetItems } from "./defaultGetItems"; -import { - MantineSuggestionMenu, - MantineSuggestionMenuProps, -} from "./MantineDefaults/MantineSuggestionMenu"; -import { MantineSuggestionMenuItemProps } from "./MantineDefaults/MantineSuggestionMenuItem"; - -export type SuggestionMenuProps< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema, - Item extends { - name: string; - execute: () => void; - } = MantineSuggestionMenuItemProps -> = { - editor: BlockNoteEditor; - getItems?: ( - query: string, - closeMenu: () => void, - clearQuery: () => void - ) => Promise; - suggestionMenuComponent?: FC>; -} & Omit & - Pick< - BlockNoteEditor["suggestionMenus"], - "closeMenu" | "clearQuery" - >; +import { SuggestionMenuProps } from "./types"; export function DefaultSuggestionMenu< - BSchema extends BlockSchema = DefaultBlockSchema, - I extends InlineContentSchema = DefaultInlineContentSchema, - S extends StyleSchema = DefaultStyleSchema, - Item extends { - name: string; - execute: () => void; - } = MantineSuggestionMenuItemProps ->(props: SuggestionMenuProps) { + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, + Item +>(props: { + editor: BlockNoteEditor; + query: string; + closeMenu: () => void; + clearQuery: () => void; + getItems: (query: string) => Promise; + onItemClick?: (item: Item) => void; + suggestionMenuComponent: FC>; +}) { const { editor, getItems, suggestionMenuComponent, query, - closeMenu, clearQuery, + closeMenu, + onItemClick, } = props; - const getItemsForLoading = useMemo<(query: string) => Promise>( - () => (query: string) => - getItems !== undefined - ? getItems(query, closeMenu, clearQuery) - : (defaultGetItems(editor, query, closeMenu, clearQuery) as Promise< - Item[] - >), - [clearQuery, closeMenu, editor, getItems] + const clickHandler = useCallback( + (item: Item) => { + closeMenu(); + clearQuery(); + onItemClick?.(item); + }, + [onItemClick, closeMenu, clearQuery] ); - const { items, usedQuery, loadingState } = useLoadSuggestionMenuItems( + const { items, usedQuery, loadingState } = useLoadSuggestionMenuItems( query, - getItemsForLoading + getItems ); useCloseSuggestionMenuNoItems(items, usedQuery, closeMenu); @@ -80,15 +54,15 @@ export function DefaultSuggestionMenu< const selectedIndex = useSuggestionMenuKeyboardNavigation( editor, items, - closeMenu + closeMenu, + onItemClick ); - const SuggestionMenuComponent: FC> = - suggestionMenuComponent || MantineSuggestionMenu; - + const Comp = suggestionMenuComponent; return ( - diff --git a/packages/react/src/components/SuggestionMenu/defaultReactSlashMenuItems.tsx b/packages/react/src/components/SuggestionMenu/defaultReactSlashMenuItems.tsx new file mode 100644 index 0000000000..34fabe491a --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/defaultReactSlashMenuItems.tsx @@ -0,0 +1,32 @@ +import { getDefaultSlashMenuItems } from "@blocknote/core"; +import { + RiH1, + RiH2, + RiH3, + RiImage2Fill, + RiListOrdered, + RiListUnordered, + RiTable2, + RiText, +} from "react-icons/ri"; + +const icons = { + "Heading 1": RiH1, + "Heading 2": RiH2, + "Heading 3": RiH3, + "Numbered List": RiListOrdered, + "Bullet List": RiListUnordered, + Paragraph: RiText, + Table: RiTable2, + Image: RiImage2Fill, +}; + +export function getDefaultReactSlashMenuItems() { + return getDefaultSlashMenuItems().map((item) => { + const Icon = icons[item.title]; + return { + ...item, + icon: , + }; + }); +} diff --git a/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts b/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts index fa9d0dfe00..1de4f7348c 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts @@ -1,15 +1,9 @@ import { useEffect, useRef } from "react"; -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 // each invalid query must be longer than the previous one to close the menu -export function useCloseSuggestionMenuNoItems< - Item extends { - name: string; - execute: () => void; - } = MantineSuggestionMenuItemProps ->( +export function useCloseSuggestionMenuNoItems( items: Item[], usedQuery: string | undefined, closeMenu: () => void, diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts index 669358bb2c..36d020f97d 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts @@ -1,19 +1,14 @@ import { BlockNoteEditor } from "@blocknote/core"; import { useEffect, useState } from "react"; -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 // menu. -export function useSuggestionMenuKeyboardNavigation< - Item extends { - name: string; - execute: () => void; - } = MantineSuggestionMenuItemProps ->( +export function useSuggestionMenuKeyboardNavigation( editor: BlockNoteEditor, items: Item[], - closeMenu: () => void + closeMenu: () => void, + onItemClick?: (item: Item) => void ) { const [selectedIndex, setSelectedIndex] = useState(0); @@ -43,7 +38,7 @@ export function useSuggestionMenuKeyboardNavigation< event.preventDefault(); if (items.length) { - items[selectedIndex].execute(); + onItemClick?.(items[selectedIndex]); } return true; @@ -73,7 +68,7 @@ export function useSuggestionMenuKeyboardNavigation< true ); }; - }, [closeMenu, editor.domElement, items, selectedIndex]); + }, [closeMenu, editor.domElement, items, selectedIndex, onItemClick]); return selectedIndex; } diff --git a/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/mantine/MantineSuggestionMenu.tsx similarity index 73% rename from packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenu.tsx rename to packages/react/src/components/SuggestionMenu/mantine/MantineSuggestionMenu.tsx index d682c04849..2030edebee 100644 --- a/packages/react/src/components/SuggestionMenu/MantineDefaults/MantineSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/mantine/MantineSuggestionMenu.tsx @@ -1,20 +1,12 @@ import { Loader, Menu } from "@mantine/core"; import { Children, useMemo } from "react"; -import { - MantineSuggestionMenuItem, - MantineSuggestionMenuItemProps, -} from "./MantineSuggestionMenuItem"; +import { DefaultSuggestionItem, SuggestionMenuProps } from "../types"; +import { MantineSuggestionMenuItem } from "./MantineSuggestionMenuItem"; -export type MantineSuggestionMenuProps = { - items: T[]; - loadingState: "loading-initial" | "loading" | "loaded"; - selectedIndex: number; -}; - -export function MantineSuggestionMenu( - props: MantineSuggestionMenuProps +export function MantineSuggestionMenu( + props: SuggestionMenuProps ) { - const { items, loadingState, selectedIndex } = props; + const { items, loadingState, selectedIndex, onItemClick } = props; const loader = loadingState === "loading-initial" || loadingState === "loading" ? ( @@ -26,8 +18,9 @@ export function MantineSuggestionMenu( const renderedItems = []; for (let i = 0; i < items.length; i++) { - if (items[i].group !== currentGroup) { - currentGroup = items[i].group; + const item = items[i]; + if (item.group !== currentGroup) { + currentGroup = item.group; renderedItems.push( {currentGroup} ); @@ -35,15 +28,16 @@ export function MantineSuggestionMenu( renderedItems.push( onItemClick?.(item)} /> ); } return renderedItems; - }, [items, selectedIndex]); + }, [items, selectedIndex, onItemClick]); return ( void; +export function MantineSuggestionMenuItem(props: { + title: string; + onClick: () => void; subtext?: string; icon?: JSX.Element; badge?: string; isSelected?: boolean; - - aliases?: string[]; - group?: string; -}; - -export function MantineSuggestionMenuItem( - props: MantineSuggestionMenuItemProps -) { +}) { const itemRef = useRef(null); function isSelected() { @@ -57,7 +50,7 @@ export function MantineSuggestionMenuItem( return ( { @@ -71,7 +64,7 @@ export function MantineSuggestionMenuItem( {/*Might need separate classes.*/} - {props.name} + {props.title} {props.subtext} diff --git a/packages/react/src/components/SuggestionMenu/types.tsx b/packages/react/src/components/SuggestionMenu/types.tsx new file mode 100644 index 0000000000..5e0f0efbb2 --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/types.tsx @@ -0,0 +1,21 @@ +/** + * Although any arbitrary data can be passed as suggestion items, the built-in + * UI components such as `MantineSuggestionMenu` expect a shape that conforms to DefaultSuggestionItem + */ +export type DefaultSuggestionItem = { + title: string; + group?: string; + subtext?: string; + icon?: JSX.Element; + badge?: string; +}; + +/** + * Props passed to a suggestion menu component + */ +export type SuggestionMenuProps = { + items: T[]; + loadingState: "loading-initial" | "loading" | "loaded"; + selectedIndex: number; + onItemClick?: (item: T) => void; +}; diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 7a7198a8ac..7358eeacfe 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -1,15 +1,17 @@ -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, + filterSuggestionItems, InlineContentSchema, StyleSchema, } from "@blocknote/core"; +import { DefaultPositionedFormattingToolbar } from "../components/FormattingToolbar/DefaultPositionedFormattingToolbar"; +import { DefaultPositionedHyperlinkToolbar } from "../components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar"; +import { DefaultPositionedImageToolbar } from "../components/ImageToolbar/DefaultPositionedImageToolbar"; +import { DefaultPositionedSideMenu } from "../components/SideMenu/DefaultPositionedSideMenu"; +import { DefaultPositionedSuggestionMenu } from "../components/SuggestionMenu/DefaultPositionedSuggestionMenu"; +import { getDefaultReactSlashMenuItems } from "../components/SuggestionMenu/defaultReactSlashMenuItems"; +import { DefaultPositionedTableHandles } from "../components/TableHandles/DefaultPositionedTableHandles"; export function BlockNoteDefaultUI< BSchema extends BlockSchema, @@ -33,7 +35,17 @@ export function BlockNoteDefaultUI< )} {props.slashMenu !== false && ( - + + filterSuggestionItems(getDefaultReactSlashMenuItems(), query) + } + // suggestionMenuComponent={MantineSuggestionMenu} + onItemClick={(item) => { + item.onItemClick(props.editor); + }} + triggerCharacter="/" + /> )} {props.sideMenu !== false && ( diff --git a/packages/react/src/hooks/useUiElementPositioning.ts b/packages/react/src/hooks/useUIElementPositioning.ts similarity index 96% rename from packages/react/src/hooks/useUiElementPositioning.ts rename to packages/react/src/hooks/useUIElementPositioning.ts index d37950ed2d..d691063120 100644 --- a/packages/react/src/hooks/useUiElementPositioning.ts +++ b/packages/react/src/hooks/useUIElementPositioning.ts @@ -7,7 +7,7 @@ import { useEffect, useMemo } from "react"; import { UiComponentPosition } from "../components-shared/UiComponentTypes"; -export function useUiElementPositioning( +export function useUIElementPositioning( show: boolean, referencePos: DOMRect | null, zIndex: number, diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 249fa885db..90d883a26f 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -20,10 +20,10 @@ export * from "./components/HyperlinkToolbar/DefaultPositionedHyperlinkToolbar"; export * from "./components/SideMenu/DefaultButtons/AddBlockButton"; export * from "./components/SideMenu/DefaultButtons/DragHandle"; +export * from "./components/SideMenu/DefaultPositionedSideMenu"; +export * from "./components/SideMenu/DefaultSideMenu"; export * from "./components/SideMenu/SideMenu"; export * from "./components/SideMenu/SideMenuButton"; -export * from "./components/SideMenu/DefaultSideMenu"; -export * from "./components/SideMenu/DefaultPositionedSideMenu"; export * from "./components/SideMenu/DragHandleMenu/DefaultButtons/BlockColorsButton"; export * from "./components/SideMenu/DragHandleMenu/DefaultButtons/RemoveBlockButton"; @@ -31,21 +31,21 @@ export * from "./components/SideMenu/DragHandleMenu/DefaultDragHandleMenu"; export * from "./components/SideMenu/DragHandleMenu/DragHandleMenu"; export * from "./components/SideMenu/DragHandleMenu/DragHandleMenuItem"; -export * from "./components/SuggestionMenu/hooks/useLoadSuggestionMenuItems"; +export * from "./components/SuggestionMenu/DefaultPositionedSuggestionMenu"; +export * from "./components/SuggestionMenu/DefaultSuggestionMenu"; +export * from "./components/SuggestionMenu/defaultReactSlashMenuItems"; export * from "./components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems"; +export * from "./components/SuggestionMenu/hooks/useLoadSuggestionMenuItems"; 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/SuggestionMenu/mantine/MantineSuggestionMenu"; +export * from "./components/SuggestionMenu/mantine/MantineSuggestionMenuItem"; export * from "./components/ImageToolbar/DefaultImageToolbar"; export * from "./components/ImageToolbar/DefaultPositionedImageToolbar"; -export * from "./components/TableHandles/hooks/useTableHandlesPositioning"; -export * from "./components/TableHandles/DefaultTableHandle"; export * from "./components/TableHandles/DefaultPositionedTableHandles"; +export * from "./components/TableHandles/DefaultTableHandle"; +export * from "./components/TableHandles/hooks/useTableHandlesPositioning"; export * from "./components-shared/Toolbar/Toolbar"; export * from "./components-shared/Toolbar/ToolbarButton"; diff --git a/packages/react/src/schema/ReactBlockSpec.tsx b/packages/react/src/schema/ReactBlockSpec.tsx index ed447edefe..ce85ef0bb2 100644 --- a/packages/react/src/schema/ReactBlockSpec.tsx +++ b/packages/react/src/schema/ReactBlockSpec.tsx @@ -101,9 +101,9 @@ export function BlockContentWrapper< // A function to create custom block for API consumers // we want to hide the tiptap node from API consumers and provide a simpler API surface instead export function createReactBlockSpec< - T extends CustomBlockConfig, - I extends InlineContentSchema, - S extends StyleSchema + const T extends CustomBlockConfig, + const I extends InlineContentSchema, + const S extends StyleSchema >( blockConfig: T, blockImplementation: ReactCustomBlockImplementation diff --git a/tests/src/utils/components/Editor.tsx b/tests/src/utils/components/Editor.tsx index fc533a1e1a..77e3880b6f 100644 --- a/tests/src/utils/components/Editor.tsx +++ b/tests/src/utils/components/Editor.tsx @@ -1,74 +1,40 @@ -import { - BlockNoteEditor, - BlockSchema, - defaultBlockSpecs, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; +import { filterSuggestionItems } from "@blocknote/core"; import "@blocknote/core/style.css"; import { BlockNoteDefaultUI, BlockNoteView, - defaultGetItems, DefaultPositionedSuggestionMenu, + getDefaultReactSlashMenuItems, useBlockNote, } from "@blocknote/react"; import { Alert, insertAlert } from "../customblocks/Alert"; -import { Button, insertButton } from "../customblocks/Button"; -import { Embed, insertEmbed } from "../customblocks/Embed"; -import { Image, insertImage } from "../customblocks/Image"; -import { Separator, insertSeparator } from "../customblocks/Separator"; +import { Button } from "../customblocks/Button"; import styles from "./Editor.module.css"; type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; const blockSpecs = { - ...defaultBlockSpecs, + // ...defaultBlockSpecs, alert: Alert, button: Button, - embed: Embed, - image: Image, - separator: Separator, + // embed: Embed, + // image: Image, + // separator: Separator, // toc: TableOfContents, }; -const getSlashMenuItems = async < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - query: string, - closeMenu: () => void, - clearQuery: () => void -) => { - const defaultItems = await defaultGetItems( - editor, - query, - closeMenu, - clearQuery - ); +const defaultItems = getDefaultReactSlashMenuItems(); - const customItems = [ - insertAlert, - insertButton, - insertEmbed, - insertImage, - insertSeparator, - // insertTableOfContents, - ] - .map((getItem) => getItem(editor, closeMenu, clearQuery)) - .filter( - ({ name, aliases }) => - name.toLowerCase().startsWith(query.toLowerCase()) || - (aliases && - aliases.filter((alias) => - alias.toLowerCase().startsWith(query.toLowerCase()) - ).length !== 0) - ); +const customItems = [ + insertAlert, + // insertButton, + // insertEmbed, + // insertImage, + // insertSeparator, + // insertTableOfContents, +]; - return [...defaultItems, ...customItems]; -}; +const allItems = [...defaultItems, ...customItems]; export default function Editor() { const editor = useBlockNote({ @@ -82,15 +48,18 @@ export default function Editor() { // Give tests a way to get prosemirror instance (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; - + // editor.insertBlocks([{ + // type:"" + // }]) return ( - getSlashMenuItems(editor, query, closeMenu, clearQuery) - } + getItems={async (query) => filterSuggestionItems(allItems, query)} + onItemClick={(i) => i.onItemClick(editor)} + // suggestionMenuComponent={MantineSuggestionMenu} + triggerCharacter="/" /> ); diff --git a/tests/src/utils/customblocks/Alert.tsx b/tests/src/utils/customblocks/Alert.tsx index c98520845f..fcf4608c65 100644 --- a/tests/src/utils/customblocks/Alert.tsx +++ b/tests/src/utils/customblocks/Alert.tsx @@ -1,13 +1,12 @@ import { BlockNoteEditor, - BlockSchema, + BlockSchemaWithBlock, + PartialBlock, createBlockSpec, defaultProps, - InlineContentSchema, - StyleSchema, } from "@blocknote/core"; + import { RiAlertFill } from "react-icons/ri"; -import { MantineSuggestionMenuItemProps } from "@blocknote/react"; const values = { warning: { icon: "⚠️", @@ -127,41 +126,29 @@ export const Alert = createBlockSpec( } ); -export const insertAlert = < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - closeMenu: () => void, - clearQuery: () => void -) => - ({ - name: "Insert Alert", - execute: () => { - closeMenu(); - clearQuery(); +export const insertAlert = { + title: "Insert Alert", + onItemClick: (editor: BlockNoteEditor) => { + const block: PartialBlock< + BlockSchemaWithBlock<"alert", (typeof Alert)["config"]>, + any, + any + > = { + type: "alert", + }; - editor.insertBlocks( - [ - { - type: "alert", - }, - ], - editor.getTextCursorPosition().block, - "after" - ); - }, - subtext: "Insert an alert block to emphasize text", - icon: , - aliases: [ - "alert", - "notification", - "emphasize", - "warning", - "error", - "info", - "success", - ], - group: "Other", - } satisfies MantineSuggestionMenuItemProps); + editor.insertBlocks([block], editor.getTextCursorPosition().block, "after"); + }, + subtext: "Insert an alert block to emphasize text", + icon: , + aliases: [ + "alert", + "notification", + "emphasize", + "warning", + "error", + "info", + "success", + ], + group: "Other", +}; diff --git a/tests/src/utils/customblocks/Button.tsx b/tests/src/utils/customblocks/Button.tsx index 0421a5d453..e0e50507b5 100644 --- a/tests/src/utils/customblocks/Button.tsx +++ b/tests/src/utils/customblocks/Button.tsx @@ -1,12 +1,8 @@ import { BlockNoteEditor, - BlockSchema, createBlockSpec, defaultProps, - InlineContentSchema, - StyleSchema, } from "@blocknote/core"; -import { MantineSuggestionMenuItemProps } from "@blocknote/react"; import { RiRadioButtonFill } from "react-icons/ri"; export const Button = createBlockSpec( @@ -41,33 +37,21 @@ export const Button = createBlockSpec( } ); -export const insertButton = < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - closeMenu: () => void, - clearQuery: () => void -) => - ({ - name: "Insert Button", - execute: () => { - closeMenu(); - clearQuery(); - - editor.insertBlocks( - [ - { - type: "button", - }, - ], - editor.getTextCursorPosition().block, - "after" - ); - }, - subtext: "Insert a button which inserts a block below it", - icon: , - aliases: ["button", "click", "action"], - group: "Other", - } satisfies MantineSuggestionMenuItemProps); +export const insertButton = { + title: "Insert Button", + onItemClick: (editor: BlockNoteEditor) => { + editor.insertBlocks( + [ + { + type: "button", + }, + ], + editor.getTextCursorPosition().block, + "after" + ); + }, + subtext: "Insert a button which inserts a block below it", + icon: , + aliases: ["button", "click", "action"], + group: "Other", +}; diff --git a/tests/src/utils/customblocks/Embed.tsx b/tests/src/utils/customblocks/Embed.tsx index b5456dcda0..5016237235 100644 --- a/tests/src/utils/customblocks/Embed.tsx +++ b/tests/src/utils/customblocks/Embed.tsx @@ -1,12 +1,5 @@ -import { - BlockNoteEditor, - BlockSchema, - createBlockSpec, - InlineContentSchema, - PartialBlock, - StyleSchema, -} from "@blocknote/core"; -import { MantineSuggestionMenuItemProps } from "@blocknote/react"; +import { BlockNoteEditor, createBlockSpec } from "@blocknote/core"; + import { RiLayout5Fill } from "react-icons/ri"; export const Embed = createBlockSpec( @@ -35,37 +28,25 @@ export const Embed = createBlockSpec( } ); -export const insertEmbed = < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - closeMenu: () => void, - clearQuery: () => void -) => - ({ - name: "Insert Embedded Website", - execute: () => { - closeMenu(); - clearQuery(); - - const src = prompt("Enter website URL"); - editor.insertBlocks( - [ - { - type: "embed", - props: { - src: src || "https://www.youtube.com/embed/wjfuB8Xjhc4", - }, - } as PartialBlock, - ], - editor.getTextCursorPosition().block, - "after" - ); - }, - subtext: "Insert an embedded website", - icon: , - aliases: ["embedded", "website", "site", "link", "url"], - group: "Other", - } satisfies MantineSuggestionMenuItemProps); +export const insertEmbed = { + title: "Insert Embedded Website", + onItemClick: (editor: BlockNoteEditor) => { + const src = prompt("Enter website URL"); + editor.insertBlocks( + [ + { + type: "embed", + props: { + src: src || "https://www.youtube.com/embed/wjfuB8Xjhc4", + }, + }, + ], + editor.getTextCursorPosition().block, + "after" + ); + }, + subtext: "Insert an embedded website", + icon: , + aliases: ["embedded", "website", "site", "link", "url"], + group: "Other", +}; diff --git a/tests/src/utils/customblocks/Image.tsx b/tests/src/utils/customblocks/Image.tsx index 3d8419704c..36e0008f4c 100644 --- a/tests/src/utils/customblocks/Image.tsx +++ b/tests/src/utils/customblocks/Image.tsx @@ -1,13 +1,8 @@ import { BlockNoteEditor, - BlockSchema, createBlockSpec, defaultProps, - InlineContentSchema, - PartialBlock, - StyleSchema, } from "@blocknote/core"; -import { MantineSuggestionMenuItemProps } from "@blocknote/react"; import { RiImage2Fill } from "react-icons/ri"; export const Image = createBlockSpec( { @@ -53,38 +48,25 @@ export const Image = createBlockSpec( } ); -export const insertImage = < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - closeMenu: () => void, - clearQuery: () => void -) => - ({ - name: "Insert Image", - execute: () => { - closeMenu(); - clearQuery(); - - const src = - prompt("Enter image URL") || "https://via.placeholder.com/1000"; - editor.insertBlocks( - [ - { - type: "image", - props: { - src, - }, - } as PartialBlock, - ], - editor.getTextCursorPosition().block, - "after" - ); - }, - subtext: "Insert an image", - icon: , - aliases: ["image", "img", "picture", "media"], - group: "Other", - } satisfies MantineSuggestionMenuItemProps); +export const insertImage = { + title: "Insert Image", + onItemClick: (editor: BlockNoteEditor) => { + const src = prompt("Enter image URL") || "https://via.placeholder.com/1000"; + editor.insertBlocks( + [ + { + type: "image", + props: { + src, + }, + }, + ], + editor.getTextCursorPosition().block, + "after" + ); + }, + subtext: "Insert an image", + icon: , + aliases: ["image", "img", "picture", "media"], + group: "Other", +}; diff --git a/tests/src/utils/customblocks/ReactAlert.tsx b/tests/src/utils/customblocks/ReactAlert.tsx index 7a61360100..d2d3175cb4 100644 --- a/tests/src/utils/customblocks/ReactAlert.tsx +++ b/tests/src/utils/customblocks/ReactAlert.tsx @@ -1,14 +1,5 @@ -import { - BlockNoteEditor, - BlockSchema, - defaultProps, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { - createReactBlockSpec, - MantineSuggestionMenuItemProps, -} from "@blocknote/react"; +import { BlockNoteEditor, defaultProps } from "@blocknote/core"; +import { createReactBlockSpec } from "@blocknote/react"; import { useEffect, useState } from "react"; import { RiAlertFill } from "react-icons/ri"; @@ -122,44 +113,32 @@ export const ReactAlert = createReactBlockSpec( }, } ); -export const insertReactAlert = < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - closeMenu: () => void, - clearQuery: () => void -) => - ({ - name: "Insert React Alert", - execute: () => { - closeMenu(); - clearQuery(); - - editor.insertBlocks( - [ - { - type: "reactAlert", - }, - ], - editor.getTextCursorPosition().block, - "after" - ); - }, - subtext: "Insert an alert block to emphasize text", - icon: , - aliases: [ - "react", - "reactAlert", - "react alert", - "alert", - "notification", - "emphasize", - "warning", - "error", - "info", - "success", - ], - group: "Other", - } satisfies MantineSuggestionMenuItemProps); +export const insertReactAlert = { + title: "Insert React Alert", + onItemClick: (editor: BlockNoteEditor) => { + editor.insertBlocks( + [ + { + type: "reactAlert", + }, + ], + editor.getTextCursorPosition().block, + "after" + ); + }, + subtext: "Insert an alert block to emphasize text", + icon: , + aliases: [ + "react", + "reactAlert", + "react alert", + "alert", + "notification", + "emphasize", + "warning", + "error", + "info", + "success", + ], + group: "Other", +}; diff --git a/tests/src/utils/customblocks/ReactImage.tsx b/tests/src/utils/customblocks/ReactImage.tsx index 1189c00546..afd1d0facb 100644 --- a/tests/src/utils/customblocks/ReactImage.tsx +++ b/tests/src/utils/customblocks/ReactImage.tsx @@ -1,15 +1,5 @@ -import { - BlockNoteEditor, - BlockSchema, - defaultProps, - InlineContentSchema, - PartialBlock, - StyleSchema, -} from "@blocknote/core"; -import { - createReactBlockSpec, - MantineSuggestionMenuItemProps, -} from "@blocknote/react"; +import { BlockNoteEditor, defaultProps } from "@blocknote/core"; +import { createReactBlockSpec } from "@blocknote/react"; import { RiImage2Fill } from "react-icons/ri"; export const ReactImage = createReactBlockSpec( @@ -46,46 +36,33 @@ export const ReactImage = createReactBlockSpec( } ); -export const insertReactImage = < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - closeMenu: () => void, - clearQuery: () => void -) => - ({ - name: "Insert React Image", - execute: () => { - closeMenu(); - clearQuery(); - - const src = - prompt("Enter image URL") || "https://via.placeholder.com/1000"; - editor.insertBlocks( - [ - { - type: "reactImage", - props: { - src, - }, - } as PartialBlock, - ], - editor.getTextCursorPosition().block, - "after" - ); - }, - subtext: "Insert an image", - icon: , - aliases: [ - "react", - "reactImage", - "react image", - "image", - "img", - "picture", - "media", - ], - group: "Media", - } satisfies MantineSuggestionMenuItemProps); +export const insertReactImage = { + title: "Insert React Image", + onItemClick: (editor: BlockNoteEditor) => { + const src = prompt("Enter image URL") || "https://via.placeholder.com/1000"; + editor.insertBlocks( + [ + { + type: "reactImage", + props: { + src, + }, + }, + ], + editor.getTextCursorPosition().block, + "after" + ); + }, + subtext: "Insert an image", + icon: , + aliases: [ + "react", + "reactImage", + "react image", + "image", + "img", + "picture", + "media", + ], + group: "Media", +}; diff --git a/tests/src/utils/customblocks/Separator.tsx b/tests/src/utils/customblocks/Separator.tsx index 0f7bcaeae2..d1936abcd0 100644 --- a/tests/src/utils/customblocks/Separator.tsx +++ b/tests/src/utils/customblocks/Separator.tsx @@ -1,11 +1,5 @@ -import { - BlockNoteEditor, - BlockSchema, - createBlockSpec, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { MantineSuggestionMenuItemProps } from "@blocknote/react"; +import { BlockNoteEditor, createBlockSpec } from "@blocknote/core"; + import { RiSeparator } from "react-icons/ri"; export const Separator = createBlockSpec( @@ -36,33 +30,21 @@ export const Separator = createBlockSpec( } ); -export const insertSeparator = < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - closeMenu: () => void, - clearQuery: () => void -) => - ({ - name: "Insert Separator", - execute: () => { - closeMenu(); - clearQuery(); - - editor.insertBlocks( - [ - { - type: "separator", - }, - ], - editor.getTextCursorPosition().block, - "after" - ); - }, - subtext: "Insert a button which inserts a block below it", - icon: , - aliases: ["separator", "horizontal", "line", "rule"], - group: "Other", - } satisfies MantineSuggestionMenuItemProps); +export const insertSeparator = { + title: "Insert Separator", + onItemClick: (editor: BlockNoteEditor) => { + editor.insertBlocks( + [ + { + type: "separator", + }, + ], + editor.getTextCursorPosition().block, + "after" + ); + }, + subtext: "Insert a button which inserts a block below it", + icon: , + aliases: ["separator", "horizontal", "line", "rule"], + group: "Other", +};