Skip to content

Commit 6df7637

Browse files
feat: Improved block type dropdown customization (#324)
* Made the block type dropdown easier to customize * Small update
1 parent 37dfef8 commit 6df7637

File tree

3 files changed

+107
-116
lines changed

3 files changed

+107
-116
lines changed
Lines changed: 89 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { useMemo, useState } from "react";
2-
import {
3-
Block,
4-
BlockNoteEditor,
5-
BlockSchema,
6-
PartialBlock,
7-
} from "@blocknote/core";
2+
import { BlockNoteEditor, BlockSchema } from "@blocknote/core";
83
import { IconType } from "react-icons";
94
import {
105
RiH1,
@@ -15,114 +10,112 @@ import {
1510
RiText,
1611
} from "react-icons/ri";
1712

18-
import {
19-
ToolbarDropdown,
20-
ToolbarDropdownProps,
21-
} from "../../../SharedComponents/Toolbar/components/ToolbarDropdown";
13+
import { ToolbarDropdown } from "../../../SharedComponents/Toolbar/components/ToolbarDropdown";
2214
import { useEditorSelectionChange } from "../../../hooks/useEditorSelectionChange";
2315
import { useEditorContentChange } from "../../../hooks/useEditorContentChange";
16+
import { ToolbarDropdownItemProps } from "../../../SharedComponents/Toolbar/components/ToolbarDropdownItem";
2417

25-
type HeadingLevels = "1" | "2" | "3";
26-
27-
const headingIcons: Record<HeadingLevels, IconType> = {
28-
"1": RiH1,
29-
"2": RiH2,
30-
"3": RiH3,
18+
export type BlockTypeDropdownItem = {
19+
name: string;
20+
type: string;
21+
props?: Record<string, string>;
22+
icon: IconType;
3123
};
3224

33-
const shouldShow = <BSchema extends BlockSchema>(block: Block<BSchema>) => {
34-
if (block.type === "paragraph") {
35-
return true;
36-
}
37-
38-
if (block.type === "heading" && "level" in block.props) {
39-
return true;
40-
}
41-
42-
if (block.type === "bulletListItem") {
43-
return true;
44-
}
45-
46-
return block.type === "numberedListItem";
47-
};
25+
export const defaultBlockTypeDropdownItems: BlockTypeDropdownItem[] = [
26+
{
27+
name: "Paragraph",
28+
type: "paragraph",
29+
icon: RiText,
30+
},
31+
{
32+
name: "Heading 1",
33+
type: "heading",
34+
props: { level: "1" },
35+
icon: RiH1,
36+
},
37+
{
38+
name: "Heading 2",
39+
type: "heading",
40+
props: { level: "2" },
41+
icon: RiH2,
42+
},
43+
{
44+
name: "Heading 3",
45+
type: "heading",
46+
props: { level: "3" },
47+
icon: RiH3,
48+
},
49+
{
50+
name: "Bullet List",
51+
type: "bulletListItem",
52+
icon: RiListUnordered,
53+
},
54+
{
55+
name: "Numbered List",
56+
type: "numberedListItem",
57+
icon: RiListOrdered,
58+
},
59+
];
4860

4961
export const BlockTypeDropdown = <BSchema extends BlockSchema>(props: {
5062
editor: BlockNoteEditor<BSchema>;
63+
items?: BlockTypeDropdownItem[];
5164
}) => {
5265
const [block, setBlock] = useState(
5366
props.editor.getTextCursorPosition().block
5467
);
5568

56-
const dropdownItems: ToolbarDropdownProps["items"] = useMemo(() => {
57-
const items: ToolbarDropdownProps["items"] = [];
58-
59-
if ("paragraph" in props.editor.schema) {
60-
items.push({
61-
onClick: () => {
62-
props.editor.focus();
63-
props.editor.updateBlock(block, {
64-
type: "paragraph",
65-
props: {},
66-
});
67-
},
68-
text: "Paragraph",
69-
icon: RiText,
70-
isSelected: block.type === "paragraph",
71-
});
72-
}
73-
74-
if (
75-
"heading" in props.editor.schema &&
76-
"level" in props.editor.schema.heading.propSchema
77-
) {
78-
items.push(
79-
...(["1", "2", "3"] as const).map((level) => ({
80-
onClick: () => {
81-
props.editor.focus();
82-
props.editor.updateBlock(block, {
83-
type: "heading",
84-
props: { level: level },
85-
} as PartialBlock<BSchema>);
86-
},
87-
text: "Heading " + level,
88-
icon: headingIcons[level],
89-
isSelected: block.type === "heading" && block.props.level === level,
90-
}))
91-
);
92-
}
93-
94-
if ("bulletListItem" in props.editor.schema) {
95-
items.push({
96-
onClick: () => {
97-
props.editor.focus();
98-
props.editor.updateBlock(block, {
99-
type: "bulletListItem",
100-
props: {},
101-
});
102-
},
103-
text: "Bullet List",
104-
icon: RiListUnordered,
105-
isSelected: block.type === "bulletListItem",
106-
});
107-
}
69+
const filteredItems: BlockTypeDropdownItem[] = useMemo(() => {
70+
return (props.items || defaultBlockTypeDropdownItems).filter((item) => {
71+
// Checks if block type exists in the schema
72+
if (!(item.type in props.editor.schema)) {
73+
return false;
74+
}
75+
76+
// Checks if props for the block type are valid
77+
for (const [prop, value] of Object.entries(item.props || {})) {
78+
const propSchema = props.editor.schema[item.type].propSchema;
79+
80+
// Checks if the prop exists for the block type
81+
if (!(prop in propSchema)) {
82+
return false;
83+
}
84+
85+
// Checks if the prop's value is valid
86+
if (
87+
propSchema[prop].values !== undefined &&
88+
!propSchema[prop].values!.includes(value)
89+
) {
90+
return false;
91+
}
92+
}
93+
94+
return true;
95+
});
96+
}, [props.editor, props.items]);
97+
98+
const shouldShow: boolean = useMemo(
99+
() => filteredItems.find((item) => item.type === block.type) !== undefined,
100+
[block.type, filteredItems]
101+
);
108102

109-
if ("numberedListItem" in props.editor.schema) {
110-
items.push({
103+
const fullItems: ToolbarDropdownItemProps[] = useMemo(
104+
() =>
105+
filteredItems.map((item) => ({
106+
text: item.name,
107+
icon: item.icon,
111108
onClick: () => {
112109
props.editor.focus();
113110
props.editor.updateBlock(block, {
114-
type: "numberedListItem",
111+
type: item.type,
115112
props: {},
116113
});
117114
},
118-
text: "Numbered List",
119-
icon: RiListOrdered,
120-
isSelected: block.type === "numberedListItem",
121-
});
122-
}
123-
124-
return items;
125-
}, [block, props.editor]);
115+
isSelected: block.type === item.type,
116+
})),
117+
[block, filteredItems, props.editor]
118+
);
126119

127120
useEditorContentChange(props.editor, () => {
128121
setBlock(props.editor.getTextCursorPosition().block);
@@ -132,9 +125,9 @@ export const BlockTypeDropdown = <BSchema extends BlockSchema>(props: {
132125
setBlock(props.editor.getTextCursorPosition().block);
133126
});
134127

135-
if (!shouldShow(block)) {
128+
if (!shouldShow) {
136129
return null;
137130
}
138131

139-
return <ToolbarDropdown items={dropdownItems} />;
132+
return <ToolbarDropdown items={fullItems} />;
140133
};

packages/react/src/FormattingToolbar/components/DefaultFormattingToolbar.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { BlockSchema } from "@blocknote/core";
22

33
import { FormattingToolbarProps } from "./FormattingToolbarPositioner";
44
import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar";
5-
import { BlockTypeDropdown } from "./DefaultDropdowns/BlockTypeDropdown";
5+
import {
6+
BlockTypeDropdown,
7+
BlockTypeDropdownItem,
8+
} from "./DefaultDropdowns/BlockTypeDropdown";
69
import { ToggledStyleButton } from "./DefaultButtons/ToggledStyleButton";
710
import { TextAlignButton } from "./DefaultButtons/TextAlignButton";
811
import { ColorStyleButton } from "./DefaultButtons/ColorStyleButton";
@@ -13,11 +16,13 @@ import {
1316
import { CreateLinkButton } from "./DefaultButtons/CreateLinkButton";
1417

1518
export const DefaultFormattingToolbar = <BSchema extends BlockSchema>(
16-
props: FormattingToolbarProps<BSchema>
19+
props: FormattingToolbarProps<BSchema> & {
20+
blockTypeDropdownItems?: BlockTypeDropdownItem[];
21+
}
1722
) => {
1823
return (
1924
<Toolbar>
20-
<BlockTypeDropdown {...props} />
25+
<BlockTypeDropdown {...props} items={props.blockTypeDropdownItems} />
2126

2227
<ToggledStyleButton editor={props.editor} toggledStyle={"bold"} />
2328
<ToggledStyleButton editor={props.editor} toggledStyle={"italic"} />

packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,29 @@
11
import { Menu } from "@mantine/core";
2-
import { MouseEvent } from "react";
3-
import { IconType } from "react-icons";
4-
import { ToolbarDropdownItem } from "./ToolbarDropdownItem";
2+
import {
3+
ToolbarDropdownItem,
4+
ToolbarDropdownItemProps,
5+
} from "./ToolbarDropdownItem";
56
import { ToolbarDropdownTarget } from "./ToolbarDropdownTarget";
67

78
export type ToolbarDropdownProps = {
8-
items: Array<{
9-
onClick?: (e: MouseEvent) => void;
10-
text: string;
11-
icon?: IconType;
12-
isSelected?: boolean;
13-
isDisabled?: boolean;
14-
}>;
9+
items: ToolbarDropdownItemProps[];
1510
isDisabled?: boolean;
1611
};
1712

1813
export function ToolbarDropdown(props: ToolbarDropdownProps) {
19-
const { isSelected, ...activeItem } = props.items.filter(
20-
(p) => p.isSelected
21-
)[0];
14+
const selectedItem = props.items.filter((p) => p.isSelected)[0];
2215

23-
if (!activeItem) {
16+
if (!selectedItem) {
2417
return null;
2518
}
2619

2720
return (
2821
<Menu exitTransitionDuration={0} disabled={props.isDisabled}>
2922
<Menu.Target>
3023
<ToolbarDropdownTarget
31-
text={activeItem.text}
32-
icon={activeItem.icon}
33-
isDisabled={activeItem.isDisabled}
24+
text={selectedItem.text}
25+
icon={selectedItem.icon}
26+
isDisabled={selectedItem.isDisabled}
3427
/>
3528
</Menu.Target>
3629
<Menu.Dropdown>

0 commit comments

Comments
 (0)