Files
wr.do/components/email/EmailEditor.tsx
2025-10-20 11:34:05 +08:00

143 lines
4.5 KiB
TypeScript

import * as React from "react";
// --- Lib ---
import { Highlight } from "@tiptap/extension-highlight";
import { Image } from "@tiptap/extension-image";
import { TaskItem, TaskList } from "@tiptap/extension-list";
import { Subscript } from "@tiptap/extension-subscript";
import { Superscript } from "@tiptap/extension-superscript";
import { TextAlign } from "@tiptap/extension-text-align";
import { Typography } from "@tiptap/extension-typography";
import { Selection } from "@tiptap/extensions";
import { EditorContent, EditorContext, useEditor } from "@tiptap/react";
// --- Tiptap Core Extensions ---
import { StarterKit } from "@tiptap/starter-kit";
import { handleImageUpload, MAX_FILE_SIZE } from "@/lib/tiptap-utils";
import { useCursorVisibility } from "@/hooks/use-cursor-visibility";
// --- Hooks ---
import { useIsMobile } from "@/hooks/use-mobile";
import { useWindowSize } from "@/hooks/use-window-size";
import { HorizontalRule } from "@/components/shared/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension";
// --- Tiptap Node ---
import { ImageUploadNode } from "@/components/shared/tiptap/tiptap-node/image-upload-node/image-upload-node-extension";
// import content from "@/components/shared/tiptap/tiptap-templates/simple/data/content.json";
import { Toolbar } from "@/components/shared/tiptap/tiptap-ui-primitive/toolbar";
import { Icons } from "../shared/icons";
import {
MainToolbarContent,
MobileToolbarContent,
} from "../shared/tiptap/tiptap-templates/simple/simple-editor";
export function EmailEditor({
onGetEditorValue,
}: {
onGetEditorValue: (html: string, text: string) => void;
}) {
const isMobile = useIsMobile();
const { height } = useWindowSize();
const [mobileView, setMobileView] = React.useState<
"main" | "highlighter" | "link"
>("main");
const toolbarRef = React.useRef<HTMLDivElement>(null);
const editor = useEditor({
immediatelyRender: false,
shouldRerenderOnTransaction: false,
editorProps: {
attributes: {
autocomplete: "off",
autocorrect: "off",
autocapitalize: "off",
"aria-label": "Main content area, start typing to enter text.",
class: "simple-editor",
},
},
extensions: [
StarterKit.configure({
horizontalRule: false,
link: {
openOnClick: false,
enableClickSelection: true,
},
}),
HorizontalRule,
TextAlign.configure({ types: ["heading", "paragraph"] }),
TaskList,
TaskItem.configure({ nested: true }),
Highlight.configure({ multicolor: true }),
Image,
Typography,
Superscript,
Subscript,
Selection,
ImageUploadNode.configure({
accept: "image/*",
maxSize: MAX_FILE_SIZE,
limit: 3,
upload: handleImageUpload,
onError: (error) => console.error("Upload failed:", error),
}),
],
content: "Hi",
onUpdate: ({ editor }) => {
const html = editor.getHTML();
const text = editor.getText();
onGetEditorValue(html, text);
},
});
const rect = useCursorVisibility({
editor,
overlayHeight: toolbarRef.current?.getBoundingClientRect().height ?? 0,
});
React.useEffect(() => {
if (!isMobile && mobileView !== "main") {
setMobileView("main");
}
}, [isMobile, mobileView]);
editor?.getHTML();
return (
<div className="simple-editor-wrapper">
<EditorContext.Provider value={{ editor }}>
<Toolbar
ref={toolbarRef}
style={{
...(isMobile
? {
bottom: `calc(100% - ${height - rect.y}px)`,
}
: {}),
}}
>
{mobileView === "main" ? (
<MainToolbarContent
onHighlighterClick={() => setMobileView("highlighter")}
onLinkClick={() => setMobileView("link")}
isMobile={isMobile}
/>
) : (
<MobileToolbarContent
type={mobileView === "highlighter" ? "highlighter" : "link"}
onBack={() => setMobileView("main")}
/>
)}
<button
className="ml-2"
onClick={() => editor?.chain().clearContent().run()}
>
<Icons.eraser className="size-4 hover:text-red-500" />
</button>
</Toolbar>
<EditorContent
editor={editor}
role="presentation"
className="simple-editor-content"
/>
</EditorContext.Provider>
</div>
);
}