feat(editor): add UploadCoordinator skeleton with pending map and node removal
This commit is contained in:
parent
c92ba6d648
commit
e06667f792
117
libs/tiptap-editor/src/upload-coordinator.ts
Normal file
117
libs/tiptap-editor/src/upload-coordinator.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import type { Editor } from '@tiptap/core'
|
||||
|
||||
/** pending 上传条目:保留 File 供重试,blobUrl 供本地预览。 */
|
||||
interface UploadEntry {
|
||||
file: File
|
||||
blobUrl: string
|
||||
fileName: string
|
||||
}
|
||||
|
||||
/** coordinator 推给 Rust 的事件(通过 window.__tiptap_uploads.events)。 */
|
||||
export interface UploadEvent {
|
||||
kind: 'error' | 'success' | 'removed'
|
||||
uploadId: string
|
||||
fileName: string
|
||||
errorMsg?: string
|
||||
ts: number
|
||||
}
|
||||
|
||||
/** 生成 uploadId,兼容非安全上下文(无 crypto.randomUUID 时)。 */
|
||||
function genUploadId(): string {
|
||||
return crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传协调器:集中管理图片上传生命周期。
|
||||
*
|
||||
* 职责:
|
||||
* - 生成 uploadId、创建 blob URL、插入占位符节点
|
||||
* - 发起上传,成功更新节点 src、失败转 error 态
|
||||
* - 按 upload-id 定位节点更新/删除(上传完成时光标早已移走)
|
||||
* - 维护 window.__tiptap_uploads 供 Rust 轮询
|
||||
*
|
||||
* pending Map 保留 File 对象直到上传成功或显式移除,支持无限重试。
|
||||
*/
|
||||
export class UploadCoordinator {
|
||||
private pending = new Map<string, UploadEntry>()
|
||||
|
||||
constructor(
|
||||
private editor: Editor,
|
||||
private onImageUpload: (file: File) => Promise<string>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 插入上传中占位符。
|
||||
* pos 省略时插入当前选区。
|
||||
* 注意:此版本只插入节点,不发起上传(runUpload 在后续任务加入)。
|
||||
*/
|
||||
insertUploading(file: File, pos?: number): void {
|
||||
const uploadId = genUploadId()
|
||||
const blobUrl = URL.createObjectURL(file)
|
||||
this.pending.set(uploadId, { file, blobUrl, fileName: file.name })
|
||||
|
||||
this.editor.chain().focus().insertContentAt(pos ?? this.editor.state.selection.head, {
|
||||
type: 'image',
|
||||
attrs: {
|
||||
src: blobUrl,
|
||||
'data-upload-state': 'uploading',
|
||||
'data-upload-id': uploadId,
|
||||
},
|
||||
}).run()
|
||||
}
|
||||
|
||||
/** 按 uploadId 删除节点(revoke blob、清 pending)。NodeView 移除按钮 / Rust ×关闭 共用。 */
|
||||
removeUpload(uploadId: string): boolean {
|
||||
const entry = this.pending.get(uploadId)
|
||||
if (!entry) return false
|
||||
this.removeNodeByUploadId(uploadId)
|
||||
URL.revokeObjectURL(entry.blobUrl)
|
||||
this.pending.delete(uploadId)
|
||||
return true
|
||||
}
|
||||
|
||||
/** 按 uploadId 在文档中定位节点并删除。 */
|
||||
private removeNodeByUploadId(uploadId: string): void {
|
||||
let targetPos: number | null = null
|
||||
let nodeSize = 0
|
||||
this.editor.state.doc.descendants((node, pos) => {
|
||||
if (node.type.name === 'image' && node.attrs['data-upload-id'] === uploadId) {
|
||||
targetPos = pos
|
||||
nodeSize = node.nodeSize
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (targetPos !== null) {
|
||||
const tr = this.editor.state.tr.delete(targetPos, targetPos + nodeSize)
|
||||
this.editor.view.dispatch(tr)
|
||||
}
|
||||
}
|
||||
|
||||
/** 按 uploadId 定位节点并合并更新属性。 */
|
||||
private updateNodeAttrs(uploadId: string, attrs: Record<string, unknown>): void {
|
||||
let targetPos: number | null = null
|
||||
let oldAttrs: Record<string, unknown> | null = null
|
||||
this.editor.state.doc.descendants((node, pos) => {
|
||||
if (node.type.name === 'image' && node.attrs['data-upload-id'] === uploadId) {
|
||||
targetPos = pos
|
||||
oldAttrs = node.attrs
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (targetPos !== null && oldAttrs) {
|
||||
const tr = this.editor.state.tr.setNodeMarkup(
|
||||
targetPos,
|
||||
undefined,
|
||||
{ ...oldAttrs, ...attrs },
|
||||
)
|
||||
this.editor.view.dispatch(tr)
|
||||
}
|
||||
}
|
||||
|
||||
/** pending Map 查询(供内部/测试)。 */
|
||||
hasPending(uploadId: string): boolean {
|
||||
return this.pending.has(uploadId)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user