150 lines
4.4 KiB
TypeScript
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
|