11import {
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" ;
109import type { IconType } from "react-icons" ;
1110import {
1211 RiH1 ,
@@ -28,17 +27,13 @@ import {
2827 useComponentsContext ,
2928} from "../../../editor/ComponentsContext.js" ;
3029import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js" ;
31- import { useEditorContentOrSelectionChange } from "../../../hooks/useEditorContentOrSelectionChange.js" ;
3230import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks.js" ;
3331
3432export 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
4439const 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