Skip to content

Commit aaa1577

Browse files
fix: BlockTypeSelect item filtering based on schema (#2112)
1 parent 5eca8a8 commit aaa1577

File tree

5 files changed

+72
-101
lines changed

5 files changed

+72
-101
lines changed

examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ export default function App() {
6666
name: "Alert",
6767
type: "alert",
6868
icon: RiAlertFill,
69-
isSelected: (block) => block.type === "alert",
7069
} satisfies BlockTypeSelectItem,
7170
]}
7271
/>

examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,16 @@ function MUIBlockTypeSelect() {
129129
// Gets the selected item.
130130
const selectedItem = useMemo(
131131
() =>
132-
defaultBlockTypeSelectItems.find((item) =>
133-
item.isSelected(block as any),
134-
)!,
132+
defaultBlockTypeSelectItems.find((item) => {
133+
const typesMatch = item.type === block.type;
134+
const propsMatch =
135+
Object.entries(item.props || {}).filter(
136+
([propName, propValue]) =>
137+
propValue !== (block as any).props[propName],
138+
).length === 0;
139+
140+
return typesMatch && propsMatch;
141+
})!,
135142
[defaultBlockTypeSelectItems, block],
136143
);
137144

examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ export default function App() {
9999
name: "Alert",
100100
type: "alert",
101101
icon: RiAlertFill,
102-
isSelected: (block) => block.type === "alert",
103102
} satisfies BlockTypeSelectItem,
104103
]}
105104
/>

examples/06-custom-schema/07-configuring-blocks/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function App() {
3030
{
3131
type: "paragraph",
3232
content:
33-
"Notice how only heading levels 1-3 are avaiable, and toggle headings are not shown.",
33+
"Notice how only heading levels 1-3 are available, and toggle headings are not shown.",
3434
},
3535
{
3636
type: "paragraph",
Lines changed: 61 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
2-
Block,
32
BlockSchema,
43
Dictionary,
4+
editorHasBlockWithType,
55
InlineContentSchema,
66
StyleSchema,
77
} from "@blocknote/core";
8-
import { useMemo, useState } from "react";
8+
import { useMemo } from "react";
99
import type { IconType } from "react-icons";
1010
import {
1111
RiH1,
@@ -27,18 +27,13 @@ import {
2727
useComponentsContext,
2828
} from "../../../editor/ComponentsContext.js";
2929
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
30-
import { useEditorContentOrSelectionChange } from "../../../hooks/useEditorContentOrSelectionChange.js";
3130
import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks.js";
32-
import { useDictionary } from "../../../i18n/dictionary.js";
3331

3432
export type BlockTypeSelectItem = {
3533
name: string;
3634
type: string;
3735
props?: Record<string, boolean | number | string>;
3836
icon: IconType;
39-
isSelected: (
40-
block: Block<BlockSchema, InlineContentSchema, StyleSchema>,
41-
) => boolean;
4237
};
4338

4439
export const blockTypeSelectItems = (
@@ -48,139 +43,90 @@ export const blockTypeSelectItems = (
4843
name: dict.slash_menu.paragraph.title,
4944
type: "paragraph",
5045
icon: RiText,
51-
isSelected: (block) => block.type === "paragraph",
5246
},
5347
{
5448
name: dict.slash_menu.heading.title,
5549
type: "heading",
56-
props: { level: 1 },
50+
props: { level: 1, isToggleable: false },
5751
icon: RiH1,
58-
isSelected: (block) =>
59-
block.type === "heading" &&
60-
"level" in block.props &&
61-
block.props.level === 1,
6252
},
6353
{
6454
name: dict.slash_menu.heading_2.title,
6555
type: "heading",
66-
props: { level: 2 },
56+
props: { level: 2, isToggleable: false },
6757
icon: RiH2,
68-
isSelected: (block) =>
69-
block.type === "heading" &&
70-
"level" in block.props &&
71-
block.props.level === 2,
7258
},
7359
{
7460
name: dict.slash_menu.heading_3.title,
7561
type: "heading",
76-
props: { level: 3 },
62+
props: { level: 3, isToggleable: false },
7763
icon: RiH3,
78-
isSelected: (block) =>
79-
block.type === "heading" &&
80-
"level" in block.props &&
81-
block.props.level === 3,
8264
},
8365
{
8466
name: dict.slash_menu.heading_4.title,
8567
type: "heading",
86-
props: { level: 4 },
68+
props: { level: 4, isToggleable: false },
8769
icon: RiH4,
88-
isSelected: (block) =>
89-
block.type === "heading" &&
90-
"level" in block.props &&
91-
block.props.level === 4,
9270
},
9371
{
9472
name: dict.slash_menu.heading_5.title,
9573
type: "heading",
96-
props: { level: 5 },
74+
props: { level: 5, isToggleable: false },
9775
icon: RiH5,
98-
isSelected: (block) =>
99-
block.type === "heading" &&
100-
"level" in block.props &&
101-
block.props.level === 5,
10276
},
10377
{
10478
name: dict.slash_menu.heading_6.title,
10579
type: "heading",
106-
props: { level: 6 },
80+
props: { level: 6, isToggleable: false },
10781
icon: RiH6,
108-
isSelected: (block) =>
109-
block.type === "heading" &&
110-
"level" in block.props &&
111-
block.props.level === 6,
11282
},
11383
{
11484
name: dict.slash_menu.toggle_heading.title,
11585
type: "heading",
11686
props: { level: 1, isToggleable: true },
11787
icon: RiH1,
118-
isSelected: (block) =>
119-
block.type === "heading" &&
120-
"level" in block.props &&
121-
block.props.level === 1 &&
122-
"isToggleable" in block.props &&
123-
block.props.isToggleable,
12488
},
12589
{
12690
name: dict.slash_menu.toggle_heading_2.title,
12791
type: "heading",
12892
props: { level: 2, isToggleable: true },
12993
icon: RiH2,
130-
isSelected: (block) =>
131-
block.type === "heading" &&
132-
"level" in block.props &&
133-
block.props.level === 2 &&
134-
"isToggleable" in block.props &&
135-
block.props.isToggleable,
13694
},
13795
{
13896
name: dict.slash_menu.toggle_heading_3.title,
13997
type: "heading",
14098
props: { level: 3, isToggleable: true },
14199
icon: RiH3,
142-
isSelected: (block) =>
143-
block.type === "heading" &&
144-
"level" in block.props &&
145-
block.props.level === 3 &&
146-
"isToggleable" in block.props &&
147-
block.props.isToggleable,
148100
},
149101
{
150102
name: dict.slash_menu.quote.title,
151103
type: "quote",
152104
icon: RiQuoteText,
153-
isSelected: (block) => block.type === "quote",
154105
},
155106
{
156107
name: dict.slash_menu.toggle_list.title,
157108
type: "toggleListItem",
158109
icon: RiPlayList2Fill,
159-
isSelected: (block) => block.type === "toggleListItem",
160110
},
161111
{
162112
name: dict.slash_menu.bullet_list.title,
163113
type: "bulletListItem",
164114
icon: RiListUnordered,
165-
isSelected: (block) => block.type === "bulletListItem",
166115
},
167116
{
168117
name: dict.slash_menu.numbered_list.title,
169118
type: "numberedListItem",
170119
icon: RiListOrdered,
171-
isSelected: (block) => block.type === "numberedListItem",
172120
},
173121
{
174122
name: dict.slash_menu.check_list.title,
175123
type: "checkListItem",
176124
icon: RiListCheck3,
177-
isSelected: (block) => block.type === "checkListItem",
178125
},
179126
];
180127

181128
export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => {
182129
const Components = useComponentsContext()!;
183-
const dict = useDictionary();
184130

185131
const editor = useBlockNoteEditor<
186132
BlockSchema,
@@ -189,50 +135,70 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => {
189135
>();
190136

191137
const selectedBlocks = useSelectedBlocks(editor);
192-
193-
const [block, setBlock] = useState(editor.getTextCursorPosition().block);
194-
195-
const filteredItems: BlockTypeSelectItem[] = useMemo(() => {
196-
return (props.items || blockTypeSelectItems(dict)).filter(
197-
(item) => item.type in editor.schema.blockSchema,
198-
);
199-
}, [editor, dict, props.items]);
200-
201-
const shouldShow: boolean = useMemo(
202-
() => filteredItems.find((item) => item.type === block.type) !== undefined,
203-
[block.type, filteredItems],
138+
const firstSelectedBlock = selectedBlocks[0];
139+
140+
// Filters out all items in which the block type and props don't conform to
141+
// the schema.
142+
const filteredItems = useMemo(
143+
() =>
144+
(props.items || blockTypeSelectItems(editor.dictionary)).filter((item) =>
145+
editorHasBlockWithType(
146+
editor,
147+
item.type,
148+
Object.fromEntries(
149+
Object.entries(item.props || {}).map(([propName, propValue]) => [
150+
propName,
151+
typeof propValue,
152+
]),
153+
) as Record<string, "string" | "number" | "boolean">,
154+
),
155+
),
156+
[editor, props.items],
204157
);
205158

206-
const fullItems: ComponentProps["FormattingToolbar"]["Select"]["items"] =
159+
// Processes `filteredItems` to an array that can be passed to
160+
// `Components.FormattingToolbar.Select`.
161+
const selectItems: ComponentProps["FormattingToolbar"]["Select"]["items"] =
207162
useMemo(() => {
208-
const onClick = (item: BlockTypeSelectItem) => {
209-
editor.focus();
210-
211-
editor.transact(() => {
212-
for (const block of selectedBlocks) {
213-
editor.updateBlock(block, {
214-
type: item.type as any,
215-
props: item.props as any,
216-
});
217-
}
218-
});
219-
};
220-
221163
return filteredItems.map((item) => {
222164
const Icon = item.icon;
223165

166+
const typesMatch = item.type === firstSelectedBlock.type;
167+
const propsMatch =
168+
Object.entries(item.props || {}).filter(
169+
([propName, propValue]) =>
170+
propValue !== firstSelectedBlock.props[propName],
171+
).length === 0;
172+
224173
return {
225174
text: item.name,
226175
icon: <Icon size={16} />,
227-
onClick: () => onClick(item),
228-
isSelected: item.isSelected(block),
176+
onClick: () => {
177+
editor.focus();
178+
editor.transact(() => {
179+
for (const block of selectedBlocks) {
180+
editor.updateBlock(block, {
181+
type: item.type as any,
182+
props: item.props as any,
183+
});
184+
}
185+
});
186+
},
187+
isSelected: typesMatch && propsMatch,
229188
};
230189
});
231-
}, [block, filteredItems, editor, selectedBlocks]);
190+
}, [
191+
editor,
192+
filteredItems,
193+
firstSelectedBlock.props,
194+
firstSelectedBlock.type,
195+
selectedBlocks,
196+
]);
232197

233-
useEditorContentOrSelectionChange(() => {
234-
setBlock(editor.getTextCursorPosition().block);
235-
}, editor);
198+
const shouldShow: boolean = useMemo(
199+
() => selectItems.find((item) => item.isSelected) !== undefined,
200+
[selectItems],
201+
);
236202

237203
if (!shouldShow || !editor.isEditable) {
238204
return null;
@@ -241,7 +207,7 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => {
241207
return (
242208
<Components.FormattingToolbar.Select
243209
className={"bn-select"}
244-
items={fullItems}
210+
items={selectItems}
245211
/>
246212
);
247213
};

0 commit comments

Comments
 (0)