import { Editor, Extension } from "@tiptap/core"; import { UploadFunction, UploadInfo } from "@lib/core/extension/types"; import UploadingView from "@lib/core/extension/upload-manager/UploadingView.vue"; import { VueRenderer } from "@tiptap/vue-3"; import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; import { Decoration, DecorationSet, type EditorView } from "@tiptap/pm/view"; export interface UploadManagerOptions { handleUpload?: UploadFunction; } declare module "@tiptap/core" { interface Commands { uploadManager: { /** * Add an image */ upload: (uploadList: UploadInfo[]) => ReturnType; }; } } function createUploadView( info: UploadInfo, options: UploadManagerOptions, view: EditorView, editor: Editor, ) { const id = {}; const tr = view.state.tr; const file = info.file; const pos = info.pos - 1; const onSuccess = (url: string) => { const { schema } = view.state; const pos = findPlaceholder(view.state, id); if (pos == null) return; const node = schema.nodes.image?.create({ src: url }); if (!node) return; const transaction = view.state.tr .replaceWith(pos, pos, node) .setMeta(uploadKey, { remove: [id] }); view.dispatch(transaction); }; const render = () => { const rendered = new VueRenderer(UploadingView, { props: { handleUpload: options.handleUpload, onSuccess: onSuccess, file: file, }, editor, }); rendered.updateProps({ destroy: () => { rendered.destroy(); }, }); return rendered; }; if (!tr.selection.empty) { tr.deleteSelection(); } tr.setMeta(uploadKey, { add: [{ id, pos, info, render }], }); view.dispatch(tr); } export function findPlaceholder(state: EditorState, id: {}) { const decos = uploadKey.getState(state) as DecorationSet; const found = decos.find(undefined, undefined, (spec) => spec.id == id); return found.length ? found[0]?.from : null; } const uploadKey = new PluginKey("upload-manager"); export function useUploadManager(options: UploadManagerOptions) { return Extension.create({ name: "upload-manager", addCommands() { return { upload: (uploadList) => { return ({ view }) => { uploadList.forEach((info) => { // 创建vue组件 createUploadView(info, options, view, this.editor); }); return true; }; }, }; }, addProseMirrorPlugins() { const plugin = new Plugin({ key: uploadKey, props: { decorations(state) { return this.getState(state); }, }, state: { init() { return DecorationSet.empty; }, apply(tr, set) { set = set.map(tr.mapping, tr.doc); //@ts-expect-error const action = tr.getMeta(this); if (action?.add) { action.add.forEach(({ id, pos, src, render, onSuccess }) => { const rendered = render(); const deco = Decoration.widget(pos + 1, rendered.element, { id, }); set = set.add(tr.doc, [deco]); }); } else if (action?.remove) { action.remove.forEach((id) => { set = set.remove(set.find(undefined, undefined, (spec) => spec.id == id)); }); } return set; }, }, }); return [plugin]; }, }); }