feat(editor): wire UploadCoordinator and custom Image in TiptapEditorInstance

- index.ts: 替换 Image.configure 为 UploadImage(自定义扩展+NodeView)
- index.ts: editor 创建后实例化 UploadCoordinator 并注入 NodeView
- index.ts: FileHandler onPaste/onDrop 改走 coordinator.insertUploading
- index.ts: 暴露 removeUploadByUploadId 供 Rust ×关闭调用
- index.ts: destroy 清理 coordinator
- upload-image.ts: 修正 import 来源(Image 来自 extension-image, mergeAttributes 来自 core)
This commit is contained in:
xfy 2026-06-22 15:28:40 +08:00
parent 57bea05aa3
commit 5d527778a5
2 changed files with 24 additions and 30 deletions

View File

@ -2,10 +2,11 @@ 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 { TaskList, TaskItem } from '@tiptap/extension-list'
import { FileHandler } from '@tiptap/extension-file-handler'
import { SlashCommand } from './slash-command'
import { UploadCoordinator } from './upload-coordinator'
import { UploadImage, setUploadCoordinator } from './upload-image'
import './style.css'
export interface EditorOptions {
@ -27,6 +28,7 @@ class TiptapEditorInstance {
private isSourceMode = false
private sourceTextarea: HTMLTextAreaElement | null = null
private toggleButton: HTMLButtonElement | null = null
private coordinator: UploadCoordinator | null = null
constructor(container: HTMLElement, options: EditorOptions = {}) {
this.container = container
@ -81,7 +83,7 @@ class TiptapEditorInstance {
html: false,
}),
TableKit,
Image.configure({ allowBase64: true }),
UploadImage,
TaskList,
TaskItem.configure({ nested: true }),
// 把宿主注入的图片上传回调透传给斜杠命令扩展,使 /上传图片 命令可用。
@ -90,35 +92,14 @@ class TiptapEditorInstance {
}),
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) => {
const msg = err instanceof Error ? err.message : String(err)
console.error('[TiptapEditor] Upload failed:', msg)
})
})
onPaste: (_editor, files) => {
if (this.coordinator) {
files.forEach((file) => this.coordinator!.insertUploading(file))
}
},
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) => {
const msg = err instanceof Error ? err.message : String(err)
console.error('[TiptapEditor] Upload failed:', msg)
})
})
onDrop: (_editor, files, pos) => {
if (this.coordinator) {
files.forEach((file) => this.coordinator!.insertUploading(file, pos))
}
},
}),
@ -142,6 +123,12 @@ class TiptapEditorInstance {
}
},
})
// 创建上传协调器,注入给 NodeView 的 onRetry/onRemove
if (this.options.onImageUpload) {
this.coordinator = new UploadCoordinator(this.editor, this.options.onImageUpload)
setUploadCoordinator(this.coordinator)
}
}
getMarkdown(): string {
@ -235,9 +222,15 @@ class TiptapEditorInstance {
return this.editor?.isEmpty ?? true
}
/** Rust 侧"×关闭"提示时调用(通过 eval。返回是否成功删除。 */
removeUploadByUploadId(uploadId: string): boolean {
return this.coordinator?.removeUpload(uploadId) ?? false
}
destroy(): void {
this.editor?.destroy()
this.editor = null
this.coordinator = null
// 清理源码模式相关引用(容器 innerHTML 已清空DOM 会随之移除)
this.sourceTextarea = null
this.toggleButton = null

View File

@ -1,4 +1,5 @@
import { Image, mergeAttributes, type Editor } from '@tiptap/core'
import { Image } from '@tiptap/extension-image'
import { mergeAttributes, type Editor } from '@tiptap/core'
import type { Node as PMNode } from '@tiptap/pm/model'
/** NodeView 按钮点击回调注入接口。 */