perf(web-toolbar): reduce rich editor toolbar update cost

This commit is contained in:
2026-04-06 02:05:42 +08:00
parent 63298d6827
commit 019436507e
+117 -46
View File
@@ -3,7 +3,7 @@ import imageCompression from "browser-image-compression";
import type { Editor as TiptapEditor } from "@tiptap/core"; import type { Editor as TiptapEditor } from "@tiptap/core";
import Link from "@tiptap/extension-link"; import Link from "@tiptap/extension-link";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import { EditorContent, type JSONContent, useEditor } from "@tiptap/react"; import { EditorContent, type JSONContent, useEditor, useEditorState } from "@tiptap/react";
import { ResizableImage } from "@/extensions/resizable-image"; import { ResizableImage } from "@/extensions/resizable-image";
import { ResizableVideo } from "@/extensions/resizable-video"; import { ResizableVideo } from "@/extensions/resizable-video";
import { ResizableYoutube } from "@/extensions/resizable-youtube"; import { ResizableYoutube } from "@/extensions/resizable-youtube";
@@ -26,7 +26,37 @@ type ToolbarButtonProps = {
onClick: () => void; onClick: () => void;
}; };
function ToolbarButton({ label, disabled = false, active = false, onClick }: ToolbarButtonProps) { type ToolbarState = {
bold: boolean;
italic: boolean;
heading: boolean;
bulletList: boolean;
link: boolean;
};
type EditorToolbarProps = {
editor: TiptapEditor | null;
onInsertImageUrl: () => void;
onOpenImageUpload: () => void;
onInsertVideoUrl: () => void;
onOpenVideoUpload: () => void;
onSetLink: () => void;
};
const DEFAULT_TOOLBAR_STATE: ToolbarState = {
bold: false,
italic: false,
heading: false,
bulletList: false,
link: false
};
const ToolbarButton = memo(function ToolbarButton({
label,
disabled = false,
active = false,
onClick
}: ToolbarButtonProps) {
return ( return (
<button <button
type="button" type="button"
@@ -43,8 +73,84 @@ function ToolbarButton({ label, disabled = false, active = false, onClick }: Too
{label} {label}
</button> </button>
); );
});
const EditorToolbar = memo(function EditorToolbar({
editor,
onInsertImageUrl,
onOpenImageUpload,
onInsertVideoUrl,
onOpenVideoUpload,
onSetLink
}: EditorToolbarProps) {
const toolbarState =
useEditorState({
editor,
selector: ({ editor: currentEditor }) => {
if (!currentEditor) {
return DEFAULT_TOOLBAR_STATE;
} }
return {
bold: currentEditor.isActive("bold"),
italic: currentEditor.isActive("italic"),
heading: currentEditor.isActive("heading", { level: 2 }),
bulletList: currentEditor.isActive("bulletList"),
link: currentEditor.isActive("link")
};
}
}) ?? DEFAULT_TOOLBAR_STATE;
const disabled = !editor;
return (
<div className="flex flex-wrap gap-1 rounded-t-lg border border-input border-b-0 bg-muted/30 px-2 py-2">
<ToolbarButton
label={"\u7c97\u4f53"}
disabled={disabled}
active={toolbarState.bold}
onClick={() => editor?.chain().focus().toggleBold().run()}
/>
<ToolbarButton
label={"\u659c\u4f53"}
disabled={disabled}
active={toolbarState.italic}
onClick={() => editor?.chain().focus().toggleItalic().run()}
/>
<ToolbarButton
label={"\u6807\u9898"}
disabled={disabled}
active={toolbarState.heading}
onClick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
/>
<ToolbarButton
label={"\u65e0\u5e8f\u5217\u8868"}
disabled={disabled}
active={toolbarState.bulletList}
onClick={() => editor?.chain().focus().toggleBulletList().run()}
/>
<ToolbarButton
label={"\u94fe\u63a5"}
disabled={disabled}
active={toolbarState.link}
onClick={onSetLink}
/>
<ToolbarButton label={"\u56fe\u7247 URL"} disabled={disabled} onClick={onInsertImageUrl} />
<ToolbarButton
label={"\u4e0a\u4f20\u56fe\u7247"}
disabled={disabled}
onClick={onOpenImageUpload}
/>
<ToolbarButton label={"\u89c6\u9891 URL"} disabled={disabled} onClick={onInsertVideoUrl} />
<ToolbarButton
label={"\u4e0a\u4f20\u89c6\u9891"}
disabled={disabled}
onClick={onOpenVideoUpload}
/>
</div>
);
});
function resolveEditorContent( function resolveEditorContent(
valueJson: string | null, valueJson: string | null,
textFallback: string textFallback: string
@@ -231,6 +337,7 @@ export const TaskRichEditor = memo(function TaskRichEditor({
"min-h-40 rounded-b-lg border border-t-0 border-input bg-background px-3 py-2 text-sm text-foreground outline-none" "min-h-40 rounded-b-lg border border-t-0 border-input bg-background px-3 py-2 text-sm text-foreground outline-none"
} }
}, },
shouldRerenderOnTransaction: false,
onUpdate({ editor: currentEditor }) { onUpdate({ editor: currentEditor }) {
scheduleEditorChange(currentEditor); scheduleEditorChange(currentEditor);
}, },
@@ -474,41 +581,18 @@ export const TaskRichEditor = memo(function TaskRichEditor({
onChange={handleVideoFileChange} onChange={handleVideoFileChange}
/> />
<div className="flex flex-wrap gap-1 rounded-t-lg border border-input border-b-0 bg-muted/30 px-2 py-2"> <EditorToolbar
<ToolbarButton editor={editor}
label="粗体" onInsertImageUrl={handleInsertImageUrl}
disabled={!editor} onOpenImageUpload={() => imageInputRef.current?.click()}
active={editor?.isActive("bold")} onInsertVideoUrl={handleInsertVideoUrl}
onClick={() => editor?.chain().focus().toggleBold().run()} onOpenVideoUpload={() => videoInputRef.current?.click()}
/> onSetLink={() => {
<ToolbarButton
label="斜体"
disabled={!editor}
active={editor?.isActive("italic")}
onClick={() => editor?.chain().focus().toggleItalic().run()}
/>
<ToolbarButton
label="标题"
disabled={!editor}
active={editor?.isActive("heading", { level: 2 })}
onClick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
/>
<ToolbarButton
label="无序列表"
disabled={!editor}
active={editor?.isActive("bulletList")}
onClick={() => editor?.chain().focus().toggleBulletList().run()}
/>
<ToolbarButton
label="链接"
disabled={!editor}
active={editor?.isActive("link")}
onClick={() => {
if (!editor) { if (!editor) {
return; return;
} }
const url = window.prompt("请输入链接地址"); const url = window.prompt("\u8bf7\u8f93\u5165\u94fe\u63a5\u5730\u5740");
if (!url) { if (!url) {
return; return;
@@ -517,19 +601,6 @@ export const TaskRichEditor = memo(function TaskRichEditor({
editor.chain().focus().setLink({ href: url }).run(); editor.chain().focus().setLink({ href: url }).run();
}} }}
/> />
<ToolbarButton label="图片 URL" disabled={!editor} onClick={handleInsertImageUrl} />
<ToolbarButton
label="上传图片"
disabled={!editor}
onClick={() => imageInputRef.current?.click()}
/>
<ToolbarButton label="视频 URL" disabled={!editor} onClick={handleInsertVideoUrl} />
<ToolbarButton
label="上传视频"
disabled={!editor}
onClick={() => videoInputRef.current?.click()}
/>
</div>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{mediaHint ? <p className="mt-2 text-xs text-muted-foreground">{mediaHint}</p> : null} {mediaHint ? <p className="mt-2 text-xs text-muted-foreground">{mediaHint}</p> : null}
</div> </div>