Skip to content

Commit 6a0150c

Browse files
committed
Refactored almost all formatting toolbar components to use useEditorState
1 parent 916fc8c commit 6a0150c

13 files changed

+416
-392
lines changed

packages/react/src/components/FormattingToolbar/DefaultButtons/BasicTextStyleButton.tsx

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
StyleSchema,
66
formatKeyboardShortcut,
77
} from "@blocknote/core";
8-
import { useMemo, useState } from "react";
8+
import { useCallback } from "react";
99
import { IconType } from "react-icons";
1010
import {
1111
RiBold,
@@ -17,8 +17,7 @@ import {
1717

1818
import { useComponentsContext } from "../../../editor/ComponentsContext.js";
1919
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
20-
import { useEditorContentOrSelectionChange } from "../../../hooks/useEditorContentOrSelectionChange.js";
21-
import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks.js";
20+
import { useEditorState } from "../../../hooks/useEditorState.js";
2221
import { useDictionary } from "../../../i18n/dictionary.js";
2322

2423
type BasicTextStyle = "bold" | "italic" | "underline" | "strike" | "code";
@@ -63,45 +62,40 @@ export const BasicTextStyleButton = <Style extends BasicTextStyle>(props: {
6362
StyleSchema
6463
>();
6564

66-
const basicTextStyleInSchema = checkBasicTextStyleInSchema(
67-
props.basicTextStyle,
65+
const state = useEditorState({
6866
editor,
67+
selector: ({ editor }) => {
68+
// Do not show if:
69+
if (
70+
// The editor is read-only.
71+
!editor.isEditable ||
72+
// The style is not in the schema.
73+
!checkBasicTextStyleInSchema(props.basicTextStyle, editor) ||
74+
// None of the selected blocks have inline content
75+
!(
76+
editor.getSelection()?.blocks || [
77+
editor.getTextCursorPosition().block,
78+
]
79+
).find((block) => block.content !== undefined)
80+
) {
81+
return undefined;
82+
}
83+
84+
return props.basicTextStyle in editor.getActiveStyles()
85+
? { active: true }
86+
: { active: false };
87+
},
88+
});
89+
90+
const toggleStyle = useCallback(
91+
(style: typeof props.basicTextStyle) => {
92+
editor.focus();
93+
editor.toggleStyles({ [style]: true } as any);
94+
},
95+
[editor, props],
6996
);
7097

71-
const selectedBlocks = useSelectedBlocks(editor);
72-
73-
const [active, setActive] = useState<boolean>(
74-
props.basicTextStyle in editor.getActiveStyles(),
75-
);
76-
77-
useEditorContentOrSelectionChange(() => {
78-
if (basicTextStyleInSchema) {
79-
setActive(props.basicTextStyle in editor.getActiveStyles());
80-
}
81-
}, editor);
82-
83-
const toggleStyle = (style: typeof props.basicTextStyle) => {
84-
editor.focus();
85-
86-
if (!basicTextStyleInSchema) {
87-
return;
88-
}
89-
90-
if (editor.schema.styleSchema[style].propSchema !== "boolean") {
91-
throw new Error("can only toggle boolean styles");
92-
}
93-
editor.toggleStyles({ [style]: true } as any);
94-
};
95-
96-
const show = useMemo(() => {
97-
if (!basicTextStyleInSchema) {
98-
return false;
99-
}
100-
// Also don't show when none of the selected blocks have text content
101-
return !!selectedBlocks.find((block) => block.content !== undefined);
102-
}, [basicTextStyleInSchema, selectedBlocks]);
103-
104-
if (!show || !editor.isEditable) {
98+
if (state === undefined) {
10599
return null;
106100
}
107101

@@ -111,7 +105,7 @@ export const BasicTextStyleButton = <Style extends BasicTextStyle>(props: {
111105
className="bn-button"
112106
data-test={props.basicTextStyle}
113107
onClick={() => toggleStyle(props.basicTextStyle)}
114-
isSelected={active}
108+
isSelected={state.active}
115109
label={dict.formatting_toolbar[props.basicTextStyle].tooltip}
116110
mainTooltip={dict.formatting_toolbar[props.basicTextStyle].tooltip}
117111
secondaryTooltip={formatKeyboardShortcut(

packages/react/src/components/FormattingToolbar/DefaultButtons/ColorStyleButton.tsx

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ import {
44
InlineContentSchema,
55
StyleSchema,
66
} from "@blocknote/core";
7-
import { useCallback, useMemo, useState } from "react";
7+
import { useCallback } from "react";
88

99
import { useComponentsContext } from "../../../editor/ComponentsContext.js";
1010
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
11-
import { useEditorContentOrSelectionChange } from "../../../hooks/useEditorContentOrSelectionChange.js";
12-
import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks.js";
11+
import { useEditorState } from "../../../hooks/useEditorState.js";
1312
import { useDictionary } from "../../../i18n/dictionary.js";
1413
import { ColorIcon } from "../../ColorPicker/ColorIcon.js";
1514
import { ColorPicker } from "../../ColorPicker/ColorPicker.js";
@@ -53,29 +52,40 @@ export const ColorStyleButton = () => {
5352
const textColorInSchema = checkColorInSchema("text", editor);
5453
const backgroundColorInSchema = checkColorInSchema("background", editor);
5554

56-
const selectedBlocks = useSelectedBlocks(editor);
55+
const state = useEditorState({
56+
editor,
57+
selector: ({ editor }) => {
58+
// Do not show if:
59+
if (
60+
// The editor is read-only.
61+
!editor.isEditable ||
62+
// None of the selected blocks have inline content
63+
!(
64+
editor.getSelection()?.blocks || [
65+
editor.getTextCursorPosition().block,
66+
]
67+
).find((block) => block.content !== undefined)
68+
) {
69+
return undefined;
70+
}
5771

58-
const [currentTextColor, setCurrentTextColor] = useState<string>(
59-
textColorInSchema
60-
? editor.getActiveStyles().textColor || "default"
61-
: "default",
62-
);
63-
const [currentBackgroundColor, setCurrentBackgroundColor] = useState<string>(
64-
backgroundColorInSchema
65-
? editor.getActiveStyles().backgroundColor || "default"
66-
: "default",
67-
);
72+
const textColorInSchema = checkColorInSchema("text", editor);
73+
const backgroundColorInSchema = checkColorInSchema("background", editor);
74+
75+
if (!textColorInSchema && !backgroundColorInSchema) {
76+
return undefined;
77+
}
6878

69-
useEditorContentOrSelectionChange(() => {
70-
if (textColorInSchema) {
71-
setCurrentTextColor(editor.getActiveStyles().textColor || "default");
72-
}
73-
if (backgroundColorInSchema) {
74-
setCurrentBackgroundColor(
75-
editor.getActiveStyles().backgroundColor || "default",
76-
);
77-
}
78-
}, editor);
79+
return {
80+
textColor: (textColorInSchema
81+
? editor.getActiveStyles().textColor || "default"
82+
: undefined) as string | undefined,
83+
backgroundColor: (backgroundColorInSchema
84+
? editor.getActiveStyles().backgroundColor || "default"
85+
: undefined) as string | undefined,
86+
};
87+
},
88+
});
7989

8090
const setTextColor = useCallback(
8191
(color: string) => {
@@ -117,21 +127,7 @@ export const ColorStyleButton = () => {
117127
[backgroundColorInSchema, editor],
118128
);
119129

120-
const show = useMemo(() => {
121-
if (!textColorInSchema && !backgroundColorInSchema) {
122-
return false;
123-
}
124-
125-
for (const block of selectedBlocks) {
126-
if (block.content !== undefined) {
127-
return true;
128-
}
129-
}
130-
131-
return false;
132-
}, [backgroundColorInSchema, selectedBlocks, textColorInSchema]);
133-
134-
if (!show || !editor.isEditable) {
130+
if (state === undefined) {
135131
return null;
136132
}
137133

@@ -145,8 +141,8 @@ export const ColorStyleButton = () => {
145141
mainTooltip={dict.formatting_toolbar.colors.tooltip}
146142
icon={
147143
<ColorIcon
148-
textColor={currentTextColor}
149-
backgroundColor={currentBackgroundColor}
144+
textColor={state.textColor}
145+
backgroundColor={state.backgroundColor}
150146
size={20}
151147
/>
152148
}
@@ -157,17 +153,17 @@ export const ColorStyleButton = () => {
157153
>
158154
<ColorPicker
159155
text={
160-
textColorInSchema
156+
state.textColor
161157
? {
162-
color: currentTextColor,
158+
color: state.textColor,
163159
setColor: setTextColor,
164160
}
165161
: undefined
166162
}
167163
background={
168-
backgroundColorInSchema
164+
state.backgroundColor
169165
? {
170-
color: currentBackgroundColor,
166+
color: state.backgroundColor,
171167
setColor: setBackgroundColor,
172168
}
173169
: undefined

packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useMemo, useState } from "react";
1+
import { useCallback, useEffect, useState } from "react";
22
import { RiLink } from "react-icons/ri";
33

44
import {
@@ -12,8 +12,7 @@ import {
1212

1313
import { useComponentsContext } from "../../../editor/ComponentsContext.js";
1414
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
15-
import { useEditorContentOrSelectionChange } from "../../../hooks/useEditorContentOrSelectionChange.js";
16-
import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks.js";
15+
import { useEditorState } from "../../../hooks/useEditorState.js";
1716
import { useDictionary } from "../../../i18n/dictionary.js";
1817
import { EditLinkMenuItems } from "../../LinkToolbar/EditLinkMenuItems.js";
1918

@@ -45,31 +44,47 @@ export const CreateLinkButton = () => {
4544
const Components = useComponentsContext()!;
4645
const dict = useDictionary();
4746

48-
const linkInSchema = checkLinkInSchema(editor);
49-
50-
const selectedBlocks = useSelectedBlocks(editor);
51-
52-
const [opened, setOpened] = useState(false);
53-
const [url, setUrl] = useState<string>(editor.getSelectedLinkUrl() || "");
54-
const [text, setText] = useState<string>(editor.getSelectedText());
47+
const [showPopover, setShowPopover] = useState(false);
48+
49+
const state = useEditorState({
50+
editor,
51+
selector: ({ editor }) => {
52+
setShowPopover(false);
53+
54+
// Do not show if:
55+
if (
56+
// The editor is read-only.
57+
!editor.isEditable ||
58+
// Links are not in the schema.
59+
!checkLinkInSchema(editor) ||
60+
// Table cells are selected.
61+
isTableCellSelection(editor.prosemirrorState.selection) ||
62+
// None of the selected blocks have inline content
63+
!(
64+
editor.getSelection()?.blocks || [
65+
editor.getTextCursorPosition().block,
66+
]
67+
).find((block) => block.content !== undefined)
68+
) {
69+
return undefined;
70+
}
5571

56-
useEditorContentOrSelectionChange(() => {
57-
setText(editor.getSelectedText() || "");
58-
setUrl(editor.getSelectedLinkUrl() || "");
59-
}, editor);
72+
return {
73+
url: editor.getSelectedLinkUrl(),
74+
text: editor.getSelectedText(),
75+
};
76+
},
77+
});
6078

79+
// Makes Ctrl+K/Meta+K open link creation popover.
6180
useEffect(() => {
6281
const callback = (event: KeyboardEvent) => {
6382
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
64-
setOpened(true);
83+
setShowPopover(true);
6584
event.preventDefault();
6685
}
6786
};
6887

69-
if (editor.headless) {
70-
return;
71-
}
72-
7388
editor.prosemirrorView.dom.addEventListener("keydown", callback);
7489

7590
return () => {
@@ -80,40 +95,17 @@ export const CreateLinkButton = () => {
8095
const update = useCallback(
8196
(url: string) => {
8297
editor.createLink(url);
83-
setOpened(false);
8498
editor.focus();
8599
},
86100
[editor],
87101
);
88102

89-
const isTableSelection = editor.transact((tr) =>
90-
isTableCellSelection(tr.selection),
91-
);
92-
93-
const show = useMemo(() => {
94-
if (!linkInSchema) {
95-
return false;
96-
}
97-
98-
for (const block of selectedBlocks) {
99-
if (block.content === undefined) {
100-
return false;
101-
}
102-
}
103-
104-
return !isTableSelection;
105-
}, [linkInSchema, selectedBlocks, isTableSelection]);
106-
107-
if (
108-
!show ||
109-
!("link" in editor.schema.inlineContentSchema) ||
110-
!editor.isEditable
111-
) {
103+
if (state === undefined) {
112104
return null;
113105
}
114106

115107
return (
116-
<Components.Generic.Popover.Root opened={opened}>
108+
<Components.Generic.Popover.Root opened={showPopover}>
117109
<Components.Generic.Popover.Trigger>
118110
{/* TODO: hide tooltip on click */}
119111
<Components.FormattingToolbar.Button
@@ -126,16 +118,16 @@ export const CreateLinkButton = () => {
126118
dict.generic.ctrl_shortcut,
127119
)}
128120
icon={<RiLink />}
129-
onClick={() => setOpened(true)}
121+
onClick={() => setShowPopover(true)}
130122
/>
131123
</Components.Generic.Popover.Trigger>
132124
<Components.Generic.Popover.Content
133125
className={"bn-popover-content bn-form-popover"}
134126
variant={"form-popover"}
135127
>
136128
<EditLinkMenuItems
137-
url={url}
138-
text={text}
129+
url={state.url || ""}
130+
text={state.text}
139131
editLink={update}
140132
showTextField={false}
141133
/>

0 commit comments

Comments
 (0)