import { Extension } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; import type { UploadResult } from "@/shared/hooks/use-file-upload"; // eslint-disable-next-line @typescript-eslint/no-explicit-any function removeImageBySrc(editor: any, src: string) { if (!editor) return; const { tr } = editor.state; let deleted = false; editor.state.doc.descendants((node: any, pos: number) => { if (deleted) return false; if (node.type.name === "image" && node.attrs.src === src) { tr.delete(pos, pos + node.nodeSize); deleted = true; return false; } }); if (deleted) editor.view.dispatch(tr); } /** * Shared upload flow: insert blob preview → upload → replace with real URL. * Used by both paste/drop (at cursor) and button upload (at end of doc). */ export async function uploadAndInsertFile( // eslint-disable-next-line @typescript-eslint/no-explicit-any editor: any, file: File, handler: (file: File) => Promise, pos?: number, ) { const isImage = file.type.startsWith("image/"); if (isImage) { const blobUrl = URL.createObjectURL(file); const imgAttrs = { src: blobUrl, alt: file.name, uploading: true }; if (pos !== undefined) { editor.chain().focus().insertContentAt(pos, { type: "image", attrs: imgAttrs }).run(); } else { editor.chain().focus().setImage(imgAttrs).run(); } try { const result = await handler(file); if (result) { const { tr } = editor.state; editor.state.doc.descendants((node: { type: { name: string }; attrs: { src: string } }, nodePos: number) => { if (node.type.name === "image" && node.attrs.src === blobUrl) { tr.setNodeMarkup(nodePos, undefined, { ...node.attrs, src: result.link, alt: result.filename, uploading: false, }); } }); editor.view.dispatch(tr); } else { removeImageBySrc(editor, blobUrl); } } catch { removeImageBySrc(editor, blobUrl); } finally { URL.revokeObjectURL(blobUrl); } } else { // Non-image: upload first, then insert link const result = await handler(file); if (!result) return; const linkText = `[${result.filename}](${result.link})`; if (pos !== undefined) { editor.chain().focus().insertContentAt(pos, linkText).run(); } else { editor.chain().focus().insertContent(linkText).run(); } } } export function createFileUploadExtension( onUploadFileRef: React.RefObject<((file: File) => Promise) | undefined>, ) { return Extension.create({ name: "fileUpload", addProseMirrorPlugins() { const { editor } = this; const handleFiles = async (files: FileList) => { const handler = onUploadFileRef.current; if (!handler) return false; for (const file of Array.from(files)) { await uploadAndInsertFile(editor, file, handler); } return true; }; return [ new Plugin({ key: new PluginKey("fileUpload"), props: { handlePaste(_view, event) { const files = event.clipboardData?.files; if (!files?.length) return false; if (!onUploadFileRef.current) return false; handleFiles(files); return true; }, handleDrop(_view, event) { const files = (event as DragEvent).dataTransfer?.files; if (!files?.length) return false; if (!onUploadFileRef.current) return false; handleFiles(files); return true; }, }, }), ]; }, }); }