180 lines
5.3 KiB
TypeScript
180 lines
5.3 KiB
TypeScript
"use client"
|
||
|
||
import { Box } from "@mantine/core"
|
||
import { RichTextEditor } from "@mantine/tiptap"
|
||
import "@mantine/tiptap/styles.css"
|
||
import { IconPhoto } from "@tabler/icons-react"
|
||
import Image from "@tiptap/extension-image"
|
||
import Link from "@tiptap/extension-link"
|
||
import Placeholder from "@tiptap/extension-placeholder"
|
||
import Underline from "@tiptap/extension-underline"
|
||
import { useEditor } from "@tiptap/react"
|
||
import StarterKit from "@tiptap/starter-kit"
|
||
import { useEffect, useRef, type ChangeEvent } from "react"
|
||
import { useKeyEventStore } from "../store"
|
||
import { useTopToolsStore } from "../useTopToolsStore"
|
||
|
||
export const splitWord = "@1024-"
|
||
|
||
const getEditorHtml = (rawValue?: string) => {
|
||
if (!rawValue) return ""
|
||
const [html] = rawValue.split(splitWord)
|
||
return html ?? rawValue
|
||
}
|
||
|
||
const editorLabels = {
|
||
boldControlLabel: "加粗",
|
||
italicControlLabel: "斜体",
|
||
underlineControlLabel: "下划线",
|
||
strikeControlLabel: "删除线",
|
||
clearFormattingControlLabel: "清除格式",
|
||
codeControlLabel: "行内代码",
|
||
h1ControlLabel: "一级标题",
|
||
h2ControlLabel: "二级标题",
|
||
h3ControlLabel: "三级标题",
|
||
h4ControlLabel: "四级标题",
|
||
bulletListControlLabel: "无序列表",
|
||
orderedListControlLabel: "有序列表",
|
||
blockquoteControlLabel: "引用",
|
||
hrControlLabel: "分割线",
|
||
linkControlLabel: "插入链接",
|
||
unlinkControlLabel: "移除链接",
|
||
linkEditorInputLabel: "输入链接地址",
|
||
linkEditorInputPlaceholder: "请输入链接,例如:https://example.com",
|
||
linkEditorExternalLink: "在新标签页打开",
|
||
linkEditorInternalLink: "在当前标签页打开",
|
||
linkEditorSave: "保存",
|
||
}
|
||
|
||
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: getEditorHtml(value),
|
||
editable: !disabled && !isView,
|
||
onUpdate: ({ editor }) => {
|
||
const html = getEditorHtml(editor.getHTML())
|
||
onChange(`${html}${splitWord}${editor.getText()}`)
|
||
},
|
||
onFocus: () => {
|
||
useKeyEventStore.getState().setFocusInput(true)
|
||
},
|
||
onBlur: () => {
|
||
useKeyEventStore.getState().setFocusInput(false)
|
||
},
|
||
})
|
||
|
||
useEffect(() => {
|
||
if (editor) {
|
||
editor.setEditable(!disabled && !isView)
|
||
}
|
||
}, [disabled, isView, editor])
|
||
|
||
useEffect(() => {
|
||
if (!editor) return
|
||
const incomingHtml = getEditorHtml(value) || "<p></p>"
|
||
if (editor.getHTML() !== incomingHtml) {
|
||
// Keep editor content in sync with external state, including empty value switch.
|
||
editor.commands.setContent(incomingHtml, { emitUpdate: false })
|
||
}
|
||
}, [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}
|
||
labels={editorLabels}
|
||
style={{ border: 0 }}>
|
||
<RichTextEditor.Toolbar>
|
||
<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="上传图片"
|
||
title="上传图片">
|
||
<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
|