Files
labelimage/components/label/components/EditorContainer.tsx
2026-03-28 15:23:08 +08:00

150 lines
4.4 KiB
TypeScript

"use client"
import { useEditor } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import Underline from "@tiptap/extension-underline"
import Link from "@tiptap/extension-link"
import Image from "@tiptap/extension-image"
import Placeholder from "@tiptap/extension-placeholder"
import { RichTextEditor } from "@mantine/tiptap"
import { useTopToolsStore } from "../useTopToolsStore"
import { useKeyEventStore } from "../store"
import { useEffect, useRef, type ChangeEvent } from "react"
import { Box } from "@mantine/core"
import { IconPhoto } from "@tabler/icons-react"
import "@mantine/tiptap/styles.css"
export const splitWord = "@1024-"
interface EditorContainerProps {
value: string
onChange: (value: string) => void
color?: string
disabled?: boolean
}
const EditorContainer = ({
value,
onChange,
color,
disabled,
}: EditorContainerProps) => {
const { isView } = useTopToolsStore()
const fileInputRef = useRef<HTMLInputElement>(null)
const editor = useEditor({
immediatelyRender: false,
extensions: [
StarterKit,
Underline,
Link,
Image,
Placeholder.configure({ placeholder: "请输入内容..." }),
],
content: value,
editable: !disabled && !isView,
onUpdate: ({ editor }) => {
// Preserve splitWord format
onChange(`${editor.getHTML()}${splitWord}${editor.getText()}`)
},
onFocus: () => {
useKeyEventStore.getState().setFocusInput(true)
},
onBlur: () => {
useKeyEventStore.getState().setFocusInput(false)
},
})
useEffect(() => {
if (editor) {
editor.setEditable(!disabled && !isView)
}
}, [disabled, isView, editor])
useEffect(() => {
// Sync external value changes to editor
// Check if editor content differs from value to avoid cursor jump and loops
if (editor && value && editor.getHTML() !== value) {
// Only set content if it's substantially different.
// Note: editor.getHTML() might return different string than value (e.g. attribute order).
// Ideally we should compare content, but string comparison is a basic check.
// If value is empty string, we should clear.
editor.commands.setContent(value)
}
}, [value, editor])
const handleImageUpload = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file && editor) {
const reader = new FileReader()
reader.onload = (e) => {
const result = e.target?.result as string
editor.chain().focus().setImage({ src: result }).run()
}
reader.readAsDataURL(file)
}
}
return (
<Box
className={color}
style={{
border: "1px solid var(--mantine-color-gray-3)",
borderRadius: "8px",
padding: "4px",
}}>
<RichTextEditor editor={editor} style={{ border: 0 }}>
<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Control
onClick={() => fileInputRef.current?.click()}
aria-label="Upload image"
title="Upload image">
<IconPhoto stroke={1.5} size="1rem" />
</RichTextEditor.Control>
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content h={144} style={{ overflowY: "auto" }} />
</RichTextEditor>
<input
type="file"
ref={fileInputRef}
style={{ display: "none" }}
accept="image/*"
onChange={handleImageUpload}
/>
</Box>
)
}
export default EditorContainer