Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clean getitems #579

Merged
merged 10 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 14 additions & 43 deletions examples/editor/examples/basic/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import {
BlockNoteEditor,
DefaultBlockSchema,
defaultInlineContentSchema,
defaultInlineContentSpecs,
DefaultStyleSchema,
InlineContentSchema,
InlineContentSpecs,
uploadToTmpFilesDotOrg_DEV_ONLY,
} from "@blocknote/core";
Expand All @@ -13,7 +8,7 @@ import {
BlockNoteView,
createReactInlineContentSpec,
DefaultPositionedSuggestionMenu,
MantineSuggestionMenuItemProps,
filterSuggestionItems,
useBlockNote,
} from "@blocknote/react";
import "@blocknote/react/style.css";
Expand Down Expand Up @@ -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<MantineSuggestionMenuItemProps[]> {
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() {
Expand All @@ -107,9 +72,15 @@ export function App() {
<DefaultPositionedSuggestionMenu
editor={editor}
triggerCharacter={"@"}
getItems={(query, closeMenu, clearQuery) =>
getMentionMenuItems(editor, query, closeMenu, clearQuery)
}
getItems={async (query) => getMentionMenuItems(query)}
onItemClick={(item) => {
editor._tiptapEditor.commands.insertContent({
type: "mention",
attrs: {
user: item.title,
},
});
}}
/>
</BlockNoteView>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,67 @@
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 { FC, useCallback } from "react";

import { useUiElement } from "../../hooks/useUiElement";
import { useUiElementPosition } from "../../hooks/useUiElementPosition";
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> = A extends readonly (infer T)[] ? T : never;

type ItemType<GetItemsType extends (query: string) => Promise<any[]>> =
ArrayElement<Awaited<ReturnType<GetItemsType>>>;

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<BSchema, I, S>;
triggerCharacter?: string;
getItems?: (
query: string,
closeMenu: () => void,
clearQuery: () => void
) => Promise<Item[]>;
suggestionMenuComponent?: FC<MantineSuggestionMenuProps<Item>>;
}) {
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<any[]>
>(
props: {
editor: BlockNoteEditor<BSchema, I, S>;
triggerCharacter?: string;
YousefED marked this conversation as resolved.
Show resolved Hide resolved
getItems: GetItemsType;
onItemClick?: (item: ItemType<GetItemsType>) => void;
} & (ItemType<GetItemsType> extends DefaultSuggestionItem
? {
// can be undefined
suggestionMenuComponent?: FC<
SuggestionMenuProps<ItemType<GetItemsType>>
>;
}
: {
// getItems doesn't return DefaultSuggestionItem, so suggestionMenuComponent is required
suggestionMenuComponent: FC<
SuggestionMenuProps<ItemType<GetItemsType>>
>;
})
) {
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 = useUiElement((callback: (state: SuggestionMenuState) => void) =>
props.editor.suggestionMenus.onUpdate.bind(props.editor.suggestionMenus)(
props.triggerCharacter || "/",
props.editor.suggestionMenus.onUpdate.bind(editor.suggestionMenus)(
triggerCharacter || "/",
callback
)
);
Expand All @@ -68,6 +87,15 @@ export function DefaultPositionedSuggestionMenu<
}
);

const clickHandler = useCallback(
YousefED marked this conversation as resolved.
Show resolved Hide resolved
(item: ItemType<GetItemsType>) => {
editor.suggestionMenus.closeMenu();
editor.suggestionMenus.clearQuery();
onItemClick?.(item);
},
[onItemClick, editor.suggestionMenus]
);

if (!isMounted || !state) {
return null;
}
Expand All @@ -77,9 +105,12 @@ export function DefaultPositionedSuggestionMenu<
return (
<div ref={ref} style={style}>
<DefaultSuggestionMenu
editor={props.editor}
getItems={props.getItems}
suggestionMenuComponent={props.suggestionMenuComponent}
editor={editor}
getItems={getItems}
suggestionMenuComponent={
suggestionMenuComponent || MantineSuggestionMenu
}
onItemClick={clickHandler}
{...data}
{...callbacks}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* 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
<DefaultPositionedSuggestionMenu
getItems={async () => [{ name: "hello" }]}
editor={undefined as any}
/>
);

// valid, because getItems returns DefaultSuggestionItem so suggestionMenuComponent is optional
menu = (
<DefaultPositionedSuggestionMenu
getItems={async () => [{ title: "hello" }]}
editor={undefined as any}
/>
);

// validate type of onItemClick
menu = (
<DefaultPositionedSuggestionMenu
suggestionMenuComponent={undefined as any}
getItems={async () => [{ hello: "hello" }]}
editor={undefined as any}
onItemClick={(item) => {
console.log(item.hello);
}}
/>
);

// prevent typescript unused error
console.log("menu", menu);
});
Original file line number Diff line number Diff line change
@@ -1,94 +1,58 @@
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 } 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<BSchema, I, S>;
getItems?: (
query: string,
closeMenu: () => void,
clearQuery: () => void
) => Promise<Item[]>;
suggestionMenuComponent?: FC<MantineSuggestionMenuProps<Item>>;
} & Omit<SuggestionMenuState, keyof UiElementPosition> &
Pick<
BlockNoteEditor<any, any, any>["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, I, S, Item>) {
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema,
Item
>(props: {
editor: BlockNoteEditor<BSchema, I, S>;
query: string;
closeMenu: () => void;
clearQuery: () => void;
getItems: (query: string) => Promise<Item[]>;
onItemClick?: (item: Item) => void;
suggestionMenuComponent: FC<SuggestionMenuProps<Item>>;
}) {
const {
editor,
getItems,
suggestionMenuComponent,
query,
closeMenu,
clearQuery,
onItemClick,
} = props;

const getItemsForLoading = useMemo<(query: string) => Promise<Item[]>>(
() => (query: string) =>
getItems !== undefined
? getItems(query, closeMenu, clearQuery)
: (defaultGetItems(editor, query, closeMenu, clearQuery) as Promise<
Item[]
>),
[clearQuery, closeMenu, editor, getItems]
);

const { items, usedQuery, loadingState } = useLoadSuggestionMenuItems<Item>(
const { items, usedQuery, loadingState } = useLoadSuggestionMenuItems(
query,
getItemsForLoading
getItems
);

useCloseSuggestionMenuNoItems(items, usedQuery, closeMenu);

const selectedIndex = useSuggestionMenuKeyboardNavigation(
editor,
items,
closeMenu
closeMenu,
onItemClick
);

const SuggestionMenuComponent: FC<MantineSuggestionMenuProps<Item>> =
suggestionMenuComponent || MantineSuggestionMenu;

const Comp = suggestionMenuComponent;
return (
<SuggestionMenuComponent
<Comp
items={items}
onItemClick={onItemClick}
loadingState={loadingState}
selectedIndex={selectedIndex}
/>
Expand Down
Loading
Loading