From f95cc2c9fcfa476a07ca20b0758eb85b1b9636a5 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 17 Oct 2025 11:34:18 +0200 Subject: [PATCH 1/8] Fixed `BlockTypeSelect` not filtering items based on schema --- .../07-configuring-blocks/src/App.tsx | 2 +- .../DefaultSelects/BlockTypeSelect.tsx | 261 ++++++++---------- 2 files changed, 119 insertions(+), 144 deletions(-) diff --git a/examples/06-custom-schema/07-configuring-blocks/src/App.tsx b/examples/06-custom-schema/07-configuring-blocks/src/App.tsx index 476f0401f1..491dc8a1c1 100644 --- a/examples/06-custom-schema/07-configuring-blocks/src/App.tsx +++ b/examples/06-custom-schema/07-configuring-blocks/src/App.tsx @@ -30,7 +30,7 @@ export default function App() { { type: "paragraph", content: - "Notice how only heading levels 1-3 are avaiable, and toggle headings are not shown.", + "Notice how only heading levels 1-3 are available, and toggle headings are not shown.", }, { type: "paragraph", diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx index a2d61c7ea6..2d68c372ac 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -1,7 +1,8 @@ import { Block, + BlockNoteEditor, BlockSchema, - Dictionary, + editorHasBlockWithType, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -29,7 +30,6 @@ import { import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js"; import { useEditorContentOrSelectionChange } from "../../../hooks/useEditorContentOrSelectionChange.js"; import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks.js"; -import { useDictionary } from "../../../i18n/dictionary.js"; export type BlockTypeSelectItem = { name: string; @@ -41,146 +41,123 @@ export type BlockTypeSelectItem = { ) => boolean; }; -export const blockTypeSelectItems = ( - dict: Dictionary, -): BlockTypeSelectItem[] => [ - { - name: dict.slash_menu.paragraph.title, - type: "paragraph", - icon: RiText, - isSelected: (block) => block.type === "paragraph", - }, - { - name: dict.slash_menu.heading.title, - type: "heading", - props: { level: 1 }, - icon: RiH1, - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === 1, - }, - { - name: dict.slash_menu.heading_2.title, - type: "heading", - props: { level: 2 }, - icon: RiH2, - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === 2, - }, - { - name: dict.slash_menu.heading_3.title, - type: "heading", - props: { level: 3 }, - icon: RiH3, - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === 3, - }, - { - name: dict.slash_menu.heading_4.title, - type: "heading", - props: { level: 4 }, - icon: RiH4, - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === 4, - }, - { - name: dict.slash_menu.heading_5.title, - type: "heading", - props: { level: 5 }, - icon: RiH5, - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === 5, - }, - { - name: dict.slash_menu.heading_6.title, - type: "heading", - props: { level: 6 }, - icon: RiH6, - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === 6, - }, - { - name: dict.slash_menu.toggle_heading.title, - type: "heading", - props: { level: 1, isToggleable: true }, - icon: RiH1, - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === 1 && - "isToggleable" in block.props && - block.props.isToggleable, - }, - { - name: dict.slash_menu.toggle_heading_2.title, - type: "heading", - props: { level: 2, isToggleable: true }, - icon: RiH2, - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === 2 && - "isToggleable" in block.props && - block.props.isToggleable, - }, - { - name: dict.slash_menu.toggle_heading_3.title, - type: "heading", - props: { level: 3, isToggleable: true }, - icon: RiH3, - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === 3 && - "isToggleable" in block.props && - block.props.isToggleable, - }, - { - name: dict.slash_menu.quote.title, - type: "quote", - icon: RiQuoteText, - isSelected: (block) => block.type === "quote", - }, - { - name: dict.slash_menu.toggle_list.title, - type: "toggleListItem", - icon: RiPlayList2Fill, - isSelected: (block) => block.type === "toggleListItem", - }, - { - name: dict.slash_menu.bullet_list.title, - type: "bulletListItem", - icon: RiListUnordered, - isSelected: (block) => block.type === "bulletListItem", - }, - { - name: dict.slash_menu.numbered_list.title, - type: "numberedListItem", - icon: RiListOrdered, - isSelected: (block) => block.type === "numberedListItem", - }, - { - name: dict.slash_menu.check_list.title, - type: "checkListItem", - icon: RiListCheck3, - isSelected: (block) => block.type === "checkListItem", - }, -]; +const headingLevelIcons: Record = { + 1: RiH1, + 2: RiH2, + 3: RiH3, + 4: RiH4, + 5: RiH5, + 6: RiH6, +}; + +export function getDefaultBlockTypeSelectItems< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, +>(editor: BlockNoteEditor) { + const items: BlockTypeSelectItem[] = []; + + if (editorHasBlockWithType(editor, "paragraph")) { + items.push({ + name: editor.dictionary.slash_menu.paragraph.title, + type: "paragraph", + icon: RiText, + isSelected: (block) => block.type === "paragraph", + }); + } + + if (editorHasBlockWithType(editor, "heading", { level: "number" })) { + ( + editor.schema.blockSchema.heading.propSchema.level.values || [1, 2, 3] + ).forEach((level) => { + items.push({ + name: editor.dictionary.slash_menu[ + `heading${level === 1 ? "" : "_" + level}` as keyof typeof editor.dictionary.slash_menu + ].title, + type: "heading", + props: { level }, + icon: headingLevelIcons[level], + isSelected: (block) => + block.type === "heading" && + "level" in block.props && + block.props.level === level, + }); + }); + } + + if ( + editorHasBlockWithType(editor, "heading", { + level: "number", + isToggleable: "boolean", + }) + ) { + (editor.schema.blockSchema.heading.propSchema.level.values || [1, 2, 3]) + .filter((level) => level <= 3) + .forEach((level) => { + items.push({ + name: editor.dictionary.slash_menu[ + `toggle_heading${level === 1 ? "" : "_" + level}` as keyof typeof editor.dictionary.slash_menu + ].title, + type: "heading", + props: { level, isToggleable: true }, + icon: headingLevelIcons[level], + isSelected: (block) => + block.type === "heading" && + "level" in block.props && + block.props.level === level && + "isToggleable" in block.props && + block.props.isToggleable, + }); + }); + } + + if (editorHasBlockWithType(editor, "quote")) { + items.push({ + name: editor.dictionary.slash_menu.quote.title, + type: "quote", + icon: RiQuoteText, + isSelected: (block) => block.type === "quote", + }); + } + + if (editorHasBlockWithType(editor, "toggleListItem")) { + items.push({ + name: editor.dictionary.slash_menu.toggle_list.title, + type: "toggleListItem", + icon: RiPlayList2Fill, + isSelected: (block) => block.type === "toggleListItem", + }); + } + if (editorHasBlockWithType(editor, "bulletListItem")) { + items.push({ + name: editor.dictionary.slash_menu.bullet_list.title, + type: "bulletListItem", + icon: RiListUnordered, + isSelected: (block) => block.type === "bulletListItem", + }); + } + if (editorHasBlockWithType(editor, "numberedListItem")) { + items.push({ + name: editor.dictionary.slash_menu.numbered_list.title, + type: "numberedListItem", + icon: RiListOrdered, + isSelected: (block) => block.type === "numberedListItem", + }); + } + if (editorHasBlockWithType(editor, "checkListItem")) { + items.push({ + name: editor.dictionary.slash_menu.check_list.title, + type: "checkListItem", + icon: RiListCheck3, + isSelected: (block) => block.type === "checkListItem", + }); + } + + return items; +} export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { const Components = useComponentsContext()!; - const dict = useDictionary(); const editor = useBlockNoteEditor< BlockSchema, @@ -193,10 +170,8 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { const [block, setBlock] = useState(editor.getTextCursorPosition().block); const filteredItems: BlockTypeSelectItem[] = useMemo(() => { - return (props.items || blockTypeSelectItems(dict)).filter( - (item) => item.type in editor.schema.blockSchema, - ); - }, [editor, dict, props.items]); + return props.items || getDefaultBlockTypeSelectItems(editor); + }, [editor, props.items]); const shouldShow: boolean = useMemo( () => filteredItems.find((item) => item.type === block.type) !== undefined, From 927bade3fab08a4f6d4c11b538a50c61fb3ee094 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 17 Oct 2025 11:49:00 +0200 Subject: [PATCH 2/8] Updated examples --- .../03-formatting-toolbar-block-type-items/src/App.tsx | 4 ++-- .../13-custom-ui/src/MUIFormattingToolbar.tsx | 4 ++-- examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx index 9ac8ed37f6..64a7b8837e 100644 --- a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx +++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx @@ -4,7 +4,7 @@ import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; import { FormattingToolbarController, - blockTypeSelectItems, + getDefaultBlockTypeSelectItems, useCreateBlockNote, BlockTypeSelectItem, FormattingToolbar, @@ -60,7 +60,7 @@ export default function App() { // Sets the items in the Block Type Select. blockTypeSelectItems={[ // Gets the default Block Type Select items. - ...blockTypeSelectItems(editor.dictionary), + ...getDefaultBlockTypeSelectItems(editor), // Adds an item for the Alert block. { name: "Alert", diff --git a/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx b/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx index 21d2003d73..f793365406 100644 --- a/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx +++ b/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx @@ -1,6 +1,6 @@ import { Block } from "@blocknote/core"; import { - blockTypeSelectItems, + getDefaultBlockTypeSelectItems, useBlockNoteEditor, useEditorContentOrSelectionChange, } from "@blocknote/react"; @@ -122,7 +122,7 @@ function MUIBlockTypeSelect() { // Gets the default items for the select. const defaultBlockTypeSelectItems = useMemo( - () => blockTypeSelectItems(editor.dictionary), + () => getDefaultBlockTypeSelectItems(editor), [editor.dictionary], ); diff --git a/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx b/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx index 495356ff98..a0908b5462 100644 --- a/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx +++ b/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx @@ -12,7 +12,7 @@ import { FormattingToolbar, FormattingToolbarController, SuggestionMenuController, - blockTypeSelectItems, + getDefaultBlockTypeSelectItems, getDefaultReactSlashMenuItems, useCreateBlockNote, } from "@blocknote/react"; @@ -93,7 +93,7 @@ export default function App() { // Sets the items in the Block Type Select. blockTypeSelectItems={[ // Gets the default Block Type Select items. - ...blockTypeSelectItems(editor.dictionary), + ...getDefaultBlockTypeSelectItems(editor), // Adds an item for the Alert block. { name: "Alert", From 5118467eb750aec46d83255ece54efa659618b2e Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 23 Oct 2025 13:32:49 +0200 Subject: [PATCH 3/8] Implemented PR feedback --- .../FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx index 2d68c372ac..c3a9d5a0b2 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -72,6 +72,8 @@ export function getDefaultBlockTypeSelectItems< ).forEach((level) => { items.push({ name: editor.dictionary.slash_menu[ + // TODO: This should be cleaned up, heading level 1 has no "_1" + // suffix which makes this more complicated than necessary. `heading${level === 1 ? "" : "_" + level}` as keyof typeof editor.dictionary.slash_menu ].title, type: "heading", From 08799683f5714d4e1aa0fc0d757c9d78e8a5e4a8 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 27 Oct 2025 13:34:37 +0100 Subject: [PATCH 4/8] Refactored/cleaned up `BlockTypeSelect` --- .../DefaultSelects/BlockTypeSelect.tsx | 109 +++++++++--------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx index c3a9d5a0b2..babd4c91bf 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -1,12 +1,11 @@ import { - Block, BlockNoteEditor, BlockSchema, editorHasBlockWithType, InlineContentSchema, StyleSchema, } from "@blocknote/core"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import type { IconType } from "react-icons"; import { RiH1, @@ -28,7 +27,6 @@ import { useComponentsContext, } from "../../../editor/ComponentsContext.js"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js"; -import { useEditorContentOrSelectionChange } from "../../../hooks/useEditorContentOrSelectionChange.js"; import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks.js"; export type BlockTypeSelectItem = { @@ -36,9 +34,6 @@ export type BlockTypeSelectItem = { type: string; props?: Record; icon: IconType; - isSelected: ( - block: Block, - ) => boolean; }; const headingLevelIcons: Record = { @@ -62,7 +57,6 @@ export function getDefaultBlockTypeSelectItems< name: editor.dictionary.slash_menu.paragraph.title, type: "paragraph", icon: RiText, - isSelected: (block) => block.type === "paragraph", }); } @@ -77,12 +71,8 @@ export function getDefaultBlockTypeSelectItems< `heading${level === 1 ? "" : "_" + level}` as keyof typeof editor.dictionary.slash_menu ].title, type: "heading", - props: { level }, + props: { level, isToggleable: false }, icon: headingLevelIcons[level], - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === level, }); }); } @@ -103,12 +93,6 @@ export function getDefaultBlockTypeSelectItems< type: "heading", props: { level, isToggleable: true }, icon: headingLevelIcons[level], - isSelected: (block) => - block.type === "heading" && - "level" in block.props && - block.props.level === level && - "isToggleable" in block.props && - block.props.isToggleable, }); }); } @@ -118,7 +102,6 @@ export function getDefaultBlockTypeSelectItems< name: editor.dictionary.slash_menu.quote.title, type: "quote", icon: RiQuoteText, - isSelected: (block) => block.type === "quote", }); } @@ -127,7 +110,6 @@ export function getDefaultBlockTypeSelectItems< name: editor.dictionary.slash_menu.toggle_list.title, type: "toggleListItem", icon: RiPlayList2Fill, - isSelected: (block) => block.type === "toggleListItem", }); } if (editorHasBlockWithType(editor, "bulletListItem")) { @@ -135,7 +117,6 @@ export function getDefaultBlockTypeSelectItems< name: editor.dictionary.slash_menu.bullet_list.title, type: "bulletListItem", icon: RiListUnordered, - isSelected: (block) => block.type === "bulletListItem", }); } if (editorHasBlockWithType(editor, "numberedListItem")) { @@ -143,7 +124,6 @@ export function getDefaultBlockTypeSelectItems< name: editor.dictionary.slash_menu.numbered_list.title, type: "numberedListItem", icon: RiListOrdered, - isSelected: (block) => block.type === "numberedListItem", }); } if (editorHasBlockWithType(editor, "checkListItem")) { @@ -151,7 +131,6 @@ export function getDefaultBlockTypeSelectItems< name: editor.dictionary.slash_menu.check_list.title, type: "checkListItem", icon: RiListCheck3, - isSelected: (block) => block.type === "checkListItem", }); } @@ -168,48 +147,70 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { >(); const selectedBlocks = useSelectedBlocks(editor); - - const [block, setBlock] = useState(editor.getTextCursorPosition().block); - - const filteredItems: BlockTypeSelectItem[] = useMemo(() => { - return props.items || getDefaultBlockTypeSelectItems(editor); - }, [editor, props.items]); - - const shouldShow: boolean = useMemo( - () => filteredItems.find((item) => item.type === block.type) !== undefined, - [block.type, filteredItems], + const firstSelectedBlock = selectedBlocks[0]; + + // Filters out all items in which the block type and props don't conform to + // the schema. + const filteredItems = useMemo( + () => + (props.items || getDefaultBlockTypeSelectItems(editor)).filter((item) => + editorHasBlockWithType( + editor, + item.type, + Object.fromEntries( + Object.entries(item.props || {}).map(([propName, propValue]) => [ + propName, + typeof propValue, + ]), + ) as Record, + ), + ), + [editor, props.items], ); - const fullItems: ComponentProps["FormattingToolbar"]["Select"]["items"] = + // Processes `filteredItems` to an array that can be passed to + // `Components.FormattingToolbar.Select`. + const selectItems: ComponentProps["FormattingToolbar"]["Select"]["items"] = useMemo(() => { - const onClick = (item: BlockTypeSelectItem) => { - editor.focus(); - - editor.transact(() => { - for (const block of selectedBlocks) { - editor.updateBlock(block, { - type: item.type as any, - props: item.props as any, - }); - } - }); - }; - return filteredItems.map((item) => { const Icon = item.icon; + const typesMatch = item.type === firstSelectedBlock.type; + const propsMatch = + Object.entries(item.props || {}).filter( + ([propName, propValue]) => + propValue !== firstSelectedBlock.props[propName], + ).length === 0; + return { text: item.name, icon: , - onClick: () => onClick(item), - isSelected: item.isSelected(block), + onClick: () => { + editor.focus(); + editor.transact(() => { + for (const block of selectedBlocks) { + editor.updateBlock(block, { + type: item.type as any, + props: item.props as any, + }); + } + }); + }, + isSelected: typesMatch && propsMatch, }; }); - }, [block, filteredItems, editor, selectedBlocks]); + }, [ + editor, + filteredItems, + firstSelectedBlock.props, + firstSelectedBlock.type, + selectedBlocks, + ]); - useEditorContentOrSelectionChange(() => { - setBlock(editor.getTextCursorPosition().block); - }, editor); + const shouldShow: boolean = useMemo( + () => selectItems.find((item) => item.isSelected) !== undefined, + [selectItems], + ); if (!shouldShow || !editor.isEditable) { return null; @@ -218,7 +219,7 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { return ( ); }; From 5481ed20e23c185f151ffee462b6618648ba490b Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 27 Oct 2025 14:06:47 +0100 Subject: [PATCH 5/8] Small fix --- examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx b/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx index a0908b5462..66225aa6c0 100644 --- a/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx +++ b/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx @@ -99,7 +99,6 @@ export default function App() { name: "Alert", type: "alert", icon: RiAlertFill, - isSelected: (block) => block.type === "alert", } satisfies BlockTypeSelectItem, ]} /> From b2d2df964d1fe4f5af44f83a81698156fa398640 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 28 Oct 2025 11:56:39 +0100 Subject: [PATCH 6/8] Small fixes --- .../src/App.tsx | 1 - .../13-custom-ui/src/MUIFormattingToolbar.tsx | 13 +- .../DefaultSelects/BlockTypeSelect.tsx | 188 ++++++++---------- 3 files changed, 98 insertions(+), 104 deletions(-) diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx index 64a7b8837e..fceb68f0d0 100644 --- a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx +++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx @@ -66,7 +66,6 @@ export default function App() { name: "Alert", type: "alert", icon: RiAlertFill, - isSelected: (block) => block.type === "alert", } satisfies BlockTypeSelectItem, ]} /> diff --git a/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx b/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx index f793365406..2a843c2d55 100644 --- a/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx +++ b/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx @@ -129,9 +129,16 @@ function MUIBlockTypeSelect() { // Gets the selected item. const selectedItem = useMemo( () => - defaultBlockTypeSelectItems.find((item) => - item.isSelected(block as any), - )!, + defaultBlockTypeSelectItems.find((item) => { + const typesMatch = item.type === block.type; + const propsMatch = + Object.entries(item.props || {}).filter( + ([propName, propValue]) => + propValue !== (block as any).props[propName], + ).length === 0; + + return typesMatch && propsMatch; + })!, [defaultBlockTypeSelectItems, block], ); diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx index babd4c91bf..cb73fd1f01 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -36,106 +36,94 @@ export type BlockTypeSelectItem = { icon: IconType; }; -const headingLevelIcons: Record = { - 1: RiH1, - 2: RiH2, - 3: RiH3, - 4: RiH4, - 5: RiH5, - 6: RiH6, -}; - -export function getDefaultBlockTypeSelectItems< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, ->(editor: BlockNoteEditor) { - const items: BlockTypeSelectItem[] = []; - - if (editorHasBlockWithType(editor, "paragraph")) { - items.push({ - name: editor.dictionary.slash_menu.paragraph.title, - type: "paragraph", - icon: RiText, - }); - } - - if (editorHasBlockWithType(editor, "heading", { level: "number" })) { - ( - editor.schema.blockSchema.heading.propSchema.level.values || [1, 2, 3] - ).forEach((level) => { - items.push({ - name: editor.dictionary.slash_menu[ - // TODO: This should be cleaned up, heading level 1 has no "_1" - // suffix which makes this more complicated than necessary. - `heading${level === 1 ? "" : "_" + level}` as keyof typeof editor.dictionary.slash_menu - ].title, - type: "heading", - props: { level, isToggleable: false }, - icon: headingLevelIcons[level], - }); - }); - } - - if ( - editorHasBlockWithType(editor, "heading", { - level: "number", - isToggleable: "boolean", - }) - ) { - (editor.schema.blockSchema.heading.propSchema.level.values || [1, 2, 3]) - .filter((level) => level <= 3) - .forEach((level) => { - items.push({ - name: editor.dictionary.slash_menu[ - `toggle_heading${level === 1 ? "" : "_" + level}` as keyof typeof editor.dictionary.slash_menu - ].title, - type: "heading", - props: { level, isToggleable: true }, - icon: headingLevelIcons[level], - }); - }); - } - - if (editorHasBlockWithType(editor, "quote")) { - items.push({ - name: editor.dictionary.slash_menu.quote.title, - type: "quote", - icon: RiQuoteText, - }); - } - - if (editorHasBlockWithType(editor, "toggleListItem")) { - items.push({ - name: editor.dictionary.slash_menu.toggle_list.title, - type: "toggleListItem", - icon: RiPlayList2Fill, - }); - } - if (editorHasBlockWithType(editor, "bulletListItem")) { - items.push({ - name: editor.dictionary.slash_menu.bullet_list.title, - type: "bulletListItem", - icon: RiListUnordered, - }); - } - if (editorHasBlockWithType(editor, "numberedListItem")) { - items.push({ - name: editor.dictionary.slash_menu.numbered_list.title, - type: "numberedListItem", - icon: RiListOrdered, - }); - } - if (editorHasBlockWithType(editor, "checkListItem")) { - items.push({ - name: editor.dictionary.slash_menu.check_list.title, - type: "checkListItem", - icon: RiListCheck3, - }); - } - - return items; -} +export const getDefaultBlockTypeSelectItems = ( + editor: BlockNoteEditor, +): BlockTypeSelectItem[] => [ + { + name: editor.dictionary.slash_menu.paragraph.title, + type: "paragraph", + icon: RiText, + }, + { + name: editor.dictionary.slash_menu.heading.title, + type: "heading", + props: { level: 1, isToggleable: false }, + icon: RiH1, + }, + { + name: editor.dictionary.slash_menu.heading_2.title, + type: "heading", + props: { level: 2, isToggleable: false }, + icon: RiH2, + }, + { + name: editor.dictionary.slash_menu.heading_3.title, + type: "heading", + props: { level: 3, isToggleable: false }, + icon: RiH3, + }, + { + name: editor.dictionary.slash_menu.heading_4.title, + type: "heading", + props: { level: 4, isToggleable: false }, + icon: RiH4, + }, + { + name: editor.dictionary.slash_menu.heading_5.title, + type: "heading", + props: { level: 5, isToggleable: false }, + icon: RiH5, + }, + { + name: editor.dictionary.slash_menu.heading_6.title, + type: "heading", + props: { level: 6, isToggleable: false }, + icon: RiH6, + }, + { + name: editor.dictionary.slash_menu.toggle_heading.title, + type: "heading", + props: { level: 1, isToggleable: true }, + icon: RiH1, + }, + { + name: editor.dictionary.slash_menu.toggle_heading_2.title, + type: "heading", + props: { level: 2, isToggleable: true }, + icon: RiH2, + }, + { + name: editor.dictionary.slash_menu.toggle_heading_3.title, + type: "heading", + props: { level: 3, isToggleable: true }, + icon: RiH3, + }, + { + name: editor.dictionary.slash_menu.quote.title, + type: "quote", + icon: RiQuoteText, + }, + { + name: editor.dictionary.slash_menu.toggle_list.title, + type: "toggleListItem", + icon: RiPlayList2Fill, + }, + { + name: editor.dictionary.slash_menu.bullet_list.title, + type: "bulletListItem", + icon: RiListUnordered, + }, + { + name: editor.dictionary.slash_menu.numbered_list.title, + type: "numberedListItem", + icon: RiListOrdered, + }, + { + name: editor.dictionary.slash_menu.check_list.title, + type: "checkListItem", + icon: RiListCheck3, + }, +]; export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { const Components = useComponentsContext()!; From 7035425f6f65307e17805e90c38be4b1db7695e0 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 31 Oct 2025 14:43:52 +0100 Subject: [PATCH 7/8] Implemented PR feedback --- .../DefaultSelects/BlockTypeSelect.tsx | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx index cb73fd1f01..3aa6f3165d 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -1,6 +1,6 @@ import { - BlockNoteEditor, BlockSchema, + Dictionary, editorHasBlockWithType, InlineContentSchema, StyleSchema, @@ -36,90 +36,90 @@ export type BlockTypeSelectItem = { icon: IconType; }; -export const getDefaultBlockTypeSelectItems = ( - editor: BlockNoteEditor, +export const blockTypeSelectItems = ( + dict: Dictionary, ): BlockTypeSelectItem[] => [ { - name: editor.dictionary.slash_menu.paragraph.title, + name: dict.slash_menu.paragraph.title, type: "paragraph", icon: RiText, }, { - name: editor.dictionary.slash_menu.heading.title, + name: dict.slash_menu.heading.title, type: "heading", props: { level: 1, isToggleable: false }, icon: RiH1, }, { - name: editor.dictionary.slash_menu.heading_2.title, + name: dict.slash_menu.heading_2.title, type: "heading", props: { level: 2, isToggleable: false }, icon: RiH2, }, { - name: editor.dictionary.slash_menu.heading_3.title, + name: dict.slash_menu.heading_3.title, type: "heading", props: { level: 3, isToggleable: false }, icon: RiH3, }, { - name: editor.dictionary.slash_menu.heading_4.title, + name: dict.slash_menu.heading_4.title, type: "heading", props: { level: 4, isToggleable: false }, icon: RiH4, }, { - name: editor.dictionary.slash_menu.heading_5.title, + name: dict.slash_menu.heading_5.title, type: "heading", props: { level: 5, isToggleable: false }, icon: RiH5, }, { - name: editor.dictionary.slash_menu.heading_6.title, + name: dict.slash_menu.heading_6.title, type: "heading", props: { level: 6, isToggleable: false }, icon: RiH6, }, { - name: editor.dictionary.slash_menu.toggle_heading.title, + name: dict.slash_menu.toggle_heading.title, type: "heading", props: { level: 1, isToggleable: true }, icon: RiH1, }, { - name: editor.dictionary.slash_menu.toggle_heading_2.title, + name: dict.slash_menu.toggle_heading_2.title, type: "heading", props: { level: 2, isToggleable: true }, icon: RiH2, }, { - name: editor.dictionary.slash_menu.toggle_heading_3.title, + name: dict.slash_menu.toggle_heading_3.title, type: "heading", props: { level: 3, isToggleable: true }, icon: RiH3, }, { - name: editor.dictionary.slash_menu.quote.title, + name: dict.slash_menu.quote.title, type: "quote", icon: RiQuoteText, }, { - name: editor.dictionary.slash_menu.toggle_list.title, + name: dict.slash_menu.toggle_list.title, type: "toggleListItem", icon: RiPlayList2Fill, }, { - name: editor.dictionary.slash_menu.bullet_list.title, + name: dict.slash_menu.bullet_list.title, type: "bulletListItem", icon: RiListUnordered, }, { - name: editor.dictionary.slash_menu.numbered_list.title, + name: dict.slash_menu.numbered_list.title, type: "numberedListItem", icon: RiListOrdered, }, { - name: editor.dictionary.slash_menu.check_list.title, + name: dict.slash_menu.check_list.title, type: "checkListItem", icon: RiListCheck3, }, @@ -141,7 +141,7 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { // the schema. const filteredItems = useMemo( () => - (props.items || getDefaultBlockTypeSelectItems(editor)).filter((item) => + (props.items || blockTypeSelectItems(editor.dictionary)).filter((item) => editorHasBlockWithType( editor, item.type, From a088929e829d5a51995110499701d1fc919694f0 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 31 Oct 2025 14:52:26 +0100 Subject: [PATCH 8/8] Fixed build --- .../03-formatting-toolbar-block-type-items/src/App.tsx | 4 ++-- .../13-custom-ui/src/MUIFormattingToolbar.tsx | 4 ++-- examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx index fceb68f0d0..2ee1da2771 100644 --- a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx +++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx @@ -4,7 +4,7 @@ import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; import { FormattingToolbarController, - getDefaultBlockTypeSelectItems, + blockTypeSelectItems, useCreateBlockNote, BlockTypeSelectItem, FormattingToolbar, @@ -60,7 +60,7 @@ export default function App() { // Sets the items in the Block Type Select. blockTypeSelectItems={[ // Gets the default Block Type Select items. - ...getDefaultBlockTypeSelectItems(editor), + ...blockTypeSelectItems(editor.dictionary), // Adds an item for the Alert block. { name: "Alert", diff --git a/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx b/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx index 2a843c2d55..f795be6641 100644 --- a/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx +++ b/examples/03-ui-components/13-custom-ui/src/MUIFormattingToolbar.tsx @@ -1,6 +1,6 @@ import { Block } from "@blocknote/core"; import { - getDefaultBlockTypeSelectItems, + blockTypeSelectItems, useBlockNoteEditor, useEditorContentOrSelectionChange, } from "@blocknote/react"; @@ -122,7 +122,7 @@ function MUIBlockTypeSelect() { // Gets the default items for the select. const defaultBlockTypeSelectItems = useMemo( - () => getDefaultBlockTypeSelectItems(editor), + () => blockTypeSelectItems(editor.dictionary), [editor.dictionary], ); diff --git a/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx b/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx index 66225aa6c0..21dc29650a 100644 --- a/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx +++ b/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx @@ -12,7 +12,7 @@ import { FormattingToolbar, FormattingToolbarController, SuggestionMenuController, - getDefaultBlockTypeSelectItems, + blockTypeSelectItems, getDefaultReactSlashMenuItems, useCreateBlockNote, } from "@blocknote/react"; @@ -93,7 +93,7 @@ export default function App() { // Sets the items in the Block Type Select. blockTypeSelectItems={[ // Gets the default Block Type Select items. - ...getDefaultBlockTypeSelectItems(editor), + ...blockTypeSelectItems(editor.dictionary), // Adds an item for the Alert block. { name: "Alert",