Previously dropped images were inserted at the current cursor position instead of the actual drop position. Now uses insertContentAt(pos, ...) to place images at the correct location.
168 lines
4.5 KiB
TypeScript
168 lines
4.5 KiB
TypeScript
import { Editor } from '@tiptap/core'
|
|
import StarterKit from '@tiptap/starter-kit'
|
|
import { Markdown } from '@tiptap/markdown'
|
|
import { TableKit } from '@tiptap/extension-table'
|
|
import { Image } from '@tiptap/extension-image'
|
|
import { Link } from '@tiptap/extension-link'
|
|
import { TaskList, TaskItem } from '@tiptap/extension-list'
|
|
import { FileHandler } from '@tiptap/extension-file-handler'
|
|
import { SlashCommand } from './slash-command'
|
|
import './style.css'
|
|
|
|
export interface EditorOptions {
|
|
content?: string
|
|
placeholder?: string
|
|
onUpdate?: (markdown: string) => void
|
|
onFocus?: () => void
|
|
onBlur?: () => void
|
|
editable?: boolean
|
|
// 新增:图片上传回调
|
|
onImageUpload?: (file: File) => Promise<string>
|
|
}
|
|
|
|
class TiptapEditorInstance {
|
|
private editor: Editor | null = null
|
|
private container: HTMLElement
|
|
private options: EditorOptions
|
|
|
|
constructor(container: HTMLElement, options: EditorOptions = {}) {
|
|
this.container = container
|
|
this.options = options
|
|
this.init()
|
|
}
|
|
|
|
private init() {
|
|
const el = document.createElement('div')
|
|
el.className = 'tiptap-editor'
|
|
this.container.appendChild(el)
|
|
|
|
this.editor = new Editor({
|
|
element: el,
|
|
extensions: [
|
|
StarterKit.configure({
|
|
heading: {
|
|
levels: [1, 2, 3],
|
|
},
|
|
}),
|
|
Markdown.configure({
|
|
html: false,
|
|
}),
|
|
TableKit,
|
|
Image.configure({ allowBase64: true }),
|
|
Link.configure({
|
|
openOnClick: false,
|
|
autolink: true,
|
|
linkOnPaste: true,
|
|
HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' },
|
|
}),
|
|
TaskList,
|
|
TaskItem.configure({ nested: true }),
|
|
SlashCommand,
|
|
FileHandler.configure({
|
|
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
|
onPaste: (editor, files) => {
|
|
if (this.options.onImageUpload) {
|
|
files.forEach((file) => {
|
|
this.options.onImageUpload!(file)
|
|
.then((url) => {
|
|
editor.chain().focus().setImage({ src: url }).run()
|
|
})
|
|
.catch((err) => {
|
|
console.error('[TiptapEditor] Upload failed:', err)
|
|
})
|
|
})
|
|
}
|
|
},
|
|
onDrop: (editor, files, pos) => {
|
|
if (this.options.onImageUpload) {
|
|
files.forEach((file) => {
|
|
this.options.onImageUpload!(file)
|
|
.then((url) => {
|
|
editor.chain().focus().insertContentAt(pos, {
|
|
type: 'image',
|
|
attrs: { src: url }
|
|
}).run()
|
|
})
|
|
.catch((err) => {
|
|
console.error('[TiptapEditor] Upload failed:', err)
|
|
})
|
|
})
|
|
}
|
|
},
|
|
}),
|
|
],
|
|
content: this.options.content || '',
|
|
editable: this.options.editable !== false,
|
|
autofocus: false,
|
|
onUpdate: ({ editor }) => {
|
|
if (this.options.onUpdate) {
|
|
this.options.onUpdate(editor.getMarkdown())
|
|
}
|
|
},
|
|
onFocus: () => {
|
|
if (this.options.onFocus) {
|
|
this.options.onFocus()
|
|
}
|
|
},
|
|
onBlur: () => {
|
|
if (this.options.onBlur) {
|
|
this.options.onBlur()
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
getMarkdown(): string {
|
|
return this.editor?.getMarkdown() || ''
|
|
}
|
|
|
|
setMarkdown(content: string): void {
|
|
this.editor?.commands.setContent(content, false, { contentType: 'markdown' })
|
|
}
|
|
|
|
getHTML(): string {
|
|
return this.editor?.getHTML() || ''
|
|
}
|
|
|
|
focus(): void {
|
|
this.editor?.commands.focus()
|
|
}
|
|
|
|
blur(): void {
|
|
this.editor?.commands.blur()
|
|
}
|
|
|
|
isEmpty(): boolean {
|
|
return this.editor?.isEmpty ?? true
|
|
}
|
|
|
|
destroy(): void {
|
|
this.editor?.destroy()
|
|
this.editor = null
|
|
this.container.innerHTML = ''
|
|
}
|
|
}
|
|
|
|
const TiptapEditor = {
|
|
_instances: new Map<string, TiptapEditorInstance>(),
|
|
|
|
create(containerId: string, options: EditorOptions = {}): TiptapEditorInstance | null {
|
|
const container = document.getElementById(containerId)
|
|
if (!container) {
|
|
console.error(`[TiptapEditor] Container not found: #${containerId}`)
|
|
return null
|
|
}
|
|
|
|
const existing = this._instances.get(containerId)
|
|
if (existing) {
|
|
existing.destroy()
|
|
}
|
|
|
|
const instance = new TiptapEditorInstance(container, options)
|
|
this._instances.set(containerId, instance)
|
|
return instance
|
|
},
|
|
}
|
|
|
|
export default TiptapEditor
|