import { Plugin, PluginKey, EditorState } from 'prosemirror-state'; import { Decoration, DecorationSet } from 'prosemirror-view'; export const imageUploadKey = new PluginKey('nileImageUpload'); interface UploadMeta { add?: { id: string; pos: number }; remove?: { id: string }; } export interface ImagePluginOptions { /** Called with image files pasted or dropped into the editor. */ onImageFiles?: (files: File[], pos: number | null) => void; } function imageFilesFrom(data: DataTransfer | null): File[] { if (!data) return []; return Array.from(data.files).filter(file => file.type.startsWith('image/')); } /** * Image behaviors: routes pasted/dropped image files to the host's upload * flow and tracks "uploading…" widget decorations that survive concurrent * edits by mapping through transactions. */ export function imagePlugin(options: ImagePluginOptions = {}): Plugin { return new Plugin({ key: imageUploadKey, state: { init: () => DecorationSet.empty, apply(tr, set) { let mapped = set.map(tr.mapping, tr.doc); const meta = tr.getMeta(imageUploadKey) as UploadMeta | undefined; if (meta?.add) { const widget = document.createElement('span'); widget.className = 'nile-wysiwyg__upload-placeholder'; widget.textContent = 'Uploading image…'; mapped = mapped.add(tr.doc, [ Decoration.widget(meta.add.pos, widget, { id: meta.add.id }), ]); } if (meta?.remove) { mapped = mapped.remove( mapped.find(undefined, undefined, spec => spec.id === meta.remove!.id) ); } return mapped; }, }, props: { decorations(state) { return imageUploadKey.getState(state); }, handlePaste(_view, event) { const files = imageFilesFrom(event.clipboardData); if (files.length === 0 || !options.onImageFiles) return false; options.onImageFiles(files, null); return true; }, handleDrop(view, event) { const files = imageFilesFrom(event.dataTransfer); if (files.length === 0 || !options.onImageFiles) return false; const drop = view.posAtCoords({ left: event.clientX, top: event.clientY }); options.onImageFiles(files, drop ? drop.pos : null); return true; }, }, }); } /** Position of an upload placeholder, after mapping through edits. */ export function findUploadPos(state: EditorState, id: string): number | null { const set = imageUploadKey.getState(state); const found = set?.find(undefined, undefined, spec => spec.id === id); return found && found.length ? found[0].from : null; }