11import {
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" ;
99import type { IconType } from "react-icons" ;
1010import {
1111 RiH1 ,
@@ -27,18 +27,13 @@ import {
2727 useComponentsContext ,
2828} from "../../../editor/ComponentsContext.js" ;
2929import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js" ;
30- import { useEditorContentOrSelectionChange } from "../../../hooks/useEditorContentOrSelectionChange.js" ;
3130import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks.js" ;
32- import { useDictionary } from "../../../i18n/dictionary.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
4439export 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
181128export 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