Skip to content

Commit 0879968

Browse files
committed
Refactored/cleaned up BlockTypeSelect
1 parent 5118467 commit 0879968

File tree

1 file changed

+55
-54
lines changed

1 file changed

+55
-54
lines changed

packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import {
2-
Block,
32
BlockNoteEditor,
43
BlockSchema,
54
editorHasBlockWithType,
65
InlineContentSchema,
76
StyleSchema,
87
} from "@blocknote/core";
9-
import { useMemo, useState } from "react";
8+
import { useMemo } from "react";
109
import type { IconType } from "react-icons";
1110
import {
1211
RiH1,
@@ -28,17 +27,13 @@ import {
2827
useComponentsContext,
2928
} from "../../../editor/ComponentsContext.js";
3029
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
31-
import { useEditorContentOrSelectionChange } from "../../../hooks/useEditorContentOrSelectionChange.js";
3230
import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks.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
const headingLevelIcons: Record<any, IconType> = {
@@ -62,7 +57,6 @@ export function getDefaultBlockTypeSelectItems<
6257
name: editor.dictionary.slash_menu.paragraph.title,
6358
type: "paragraph",
6459
icon: RiText,
65-
isSelected: (block) => block.type === "paragraph",
6660
});
6761
}
6862

@@ -77,12 +71,8 @@ export function getDefaultBlockTypeSelectItems<
7771
`heading${level === 1 ? "" : "_" + level}` as keyof typeof editor.dictionary.slash_menu
7872
].title,
7973
type: "heading",
80-
props: { level },
74+
props: { level, isToggleable: false },
8175
icon: headingLevelIcons[level],
82-
isSelected: (block) =>
83-
block.type === "heading" &&
84-
"level" in block.props &&
85-
block.props.level === level,
8676
});
8777
});
8878
}
@@ -103,12 +93,6 @@ export function getDefaultBlockTypeSelectItems<
10393
type: "heading",
10494
props: { level, isToggleable: true },
10595
icon: headingLevelIcons[level],
106-
isSelected: (block) =>
107-
block.type === "heading" &&
108-
"level" in block.props &&
109-
block.props.level === level &&
110-
"isToggleable" in block.props &&
111-
block.props.isToggleable,
11296
});
11397
});
11498
}
@@ -118,7 +102,6 @@ export function getDefaultBlockTypeSelectItems<
118102
name: editor.dictionary.slash_menu.quote.title,
119103
type: "quote",
120104
icon: RiQuoteText,
121-
isSelected: (block) => block.type === "quote",
122105
});
123106
}
124107

@@ -127,31 +110,27 @@ export function getDefaultBlockTypeSelectItems<
127110
name: editor.dictionary.slash_menu.toggle_list.title,
128111
type: "toggleListItem",
129112
icon: RiPlayList2Fill,
130-
isSelected: (block) => block.type === "toggleListItem",
131113
});
132114
}
133115
if (editorHasBlockWithType(editor, "bulletListItem")) {
134116
items.push({
135117
name: editor.dictionary.slash_menu.bullet_list.title,
136118
type: "bulletListItem",
137119
icon: RiListUnordered,
138-
isSelected: (block) => block.type === "bulletListItem",
139120
});
140121
}
141122
if (editorHasBlockWithType(editor, "numberedListItem")) {
142123
items.push({
143124
name: editor.dictionary.slash_menu.numbered_list.title,
144125
type: "numberedListItem",
145126
icon: RiListOrdered,
146-
isSelected: (block) => block.type === "numberedListItem",
147127
});
148128
}
149129
if (editorHasBlockWithType(editor, "checkListItem")) {
150130
items.push({
151131
name: editor.dictionary.slash_menu.check_list.title,
152132
type: "checkListItem",
153133
icon: RiListCheck3,
154-
isSelected: (block) => block.type === "checkListItem",
155134
});
156135
}
157136

@@ -168,48 +147,70 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => {
168147
>();
169148

170149
const selectedBlocks = useSelectedBlocks(editor);
171-
172-
const [block, setBlock] = useState(editor.getTextCursorPosition().block);
173-
174-
const filteredItems: BlockTypeSelectItem[] = useMemo(() => {
175-
return props.items || getDefaultBlockTypeSelectItems(editor);
176-
}, [editor, props.items]);
177-
178-
const shouldShow: boolean = useMemo(
179-
() => filteredItems.find((item) => item.type === block.type) !== undefined,
180-
[block.type, filteredItems],
150+
const firstSelectedBlock = selectedBlocks[0];
151+
152+
// Filters out all items in which the block type and props don't conform to
153+
// the schema.
154+
const filteredItems = useMemo(
155+
() =>
156+
(props.items || getDefaultBlockTypeSelectItems(editor)).filter((item) =>
157+
editorHasBlockWithType(
158+
editor,
159+
item.type,
160+
Object.fromEntries(
161+
Object.entries(item.props || {}).map(([propName, propValue]) => [
162+
propName,
163+
typeof propValue,
164+
]),
165+
) as Record<string, "string" | "number" | "boolean">,
166+
),
167+
),
168+
[editor, props.items],
181169
);
182170

183-
const fullItems: ComponentProps["FormattingToolbar"]["Select"]["items"] =
171+
// Processes `filteredItems` to an array that can be passed to
172+
// `Components.FormattingToolbar.Select`.
173+
const selectItems: ComponentProps["FormattingToolbar"]["Select"]["items"] =
184174
useMemo(() => {
185-
const onClick = (item: BlockTypeSelectItem) => {
186-
editor.focus();
187-
188-
editor.transact(() => {
189-
for (const block of selectedBlocks) {
190-
editor.updateBlock(block, {
191-
type: item.type as any,
192-
props: item.props as any,
193-
});
194-
}
195-
});
196-
};
197-
198175
return filteredItems.map((item) => {
199176
const Icon = item.icon;
200177

178+
const typesMatch = item.type === firstSelectedBlock.type;
179+
const propsMatch =
180+
Object.entries(item.props || {}).filter(
181+
([propName, propValue]) =>
182+
propValue !== firstSelectedBlock.props[propName],
183+
).length === 0;
184+
201185
return {
202186
text: item.name,
203187
icon: <Icon size={16} />,
204-
onClick: () => onClick(item),
205-
isSelected: item.isSelected(block),
188+
onClick: () => {
189+
editor.focus();
190+
editor.transact(() => {
191+
for (const block of selectedBlocks) {
192+
editor.updateBlock(block, {
193+
type: item.type as any,
194+
props: item.props as any,
195+
});
196+
}
197+
});
198+
},
199+
isSelected: typesMatch && propsMatch,
206200
};
207201
});
208-
}, [block, filteredItems, editor, selectedBlocks]);
202+
}, [
203+
editor,
204+
filteredItems,
205+
firstSelectedBlock.props,
206+
firstSelectedBlock.type,
207+
selectedBlocks,
208+
]);
209209

210-
useEditorContentOrSelectionChange(() => {
211-
setBlock(editor.getTextCursorPosition().block);
212-
}, editor);
210+
const shouldShow: boolean = useMemo(
211+
() => selectItems.find((item) => item.isSelected) !== undefined,
212+
[selectItems],
213+
);
213214

214215
if (!shouldShow || !editor.isEditable) {
215216
return null;
@@ -218,7 +219,7 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => {
218219
return (
219220
<Components.FormattingToolbar.Select
220221
className={"bn-select"}
221-
items={fullItems}
222+
items={selectItems}
222223
/>
223224
);
224225
};

0 commit comments

Comments
 (0)