UNPKG

@firecms/core

Version:

Awesome Firebase/Firestore-based headless open-source CMS

134 lines (114 loc) 5.06 kB
import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { Plugin, PluginKey } from "prosemirror-state"; import { schema } from "../../schema"; export type UploadFn = (image: File) => Promise<string>; export async function onFileRead(view: EditorView, readerEvent: ProgressEvent<FileReader>, pos: number, upload: UploadFn, image: File) { // @ts-ignore const plugin = view.state.plugins.find((p: Plugin) => p.key === ImagePluginKey.key); if (!plugin) { console.error("Image plugin not found"); return; } let decorationSet = plugin.getState(view.state); const placeholder = document.createElement("div"); const imageElement = document.createElement("img"); // basic styling for loading state imageElement.setAttribute("class", "opacity-40 rounded-lg border"); imageElement.src = readerEvent.target?.result as string; placeholder.appendChild(imageElement); const deco = Decoration.widget(pos, placeholder); decorationSet = decorationSet?.add(view.state.doc, [deco]); view.dispatch(view.state.tr.setMeta(plugin, { decorationSet })); // Image Upload Logic const src = await upload(image); console.debug("Uploaded image", src); // Replace placeholder with actual image const imageNode = schema.nodes.image.create({ src }); const tr = view.state.tr.replaceWith(pos, pos, imageNode); // Remove placeholder decoration decorationSet = decorationSet?.remove([deco]); tr.setMeta(plugin, { decorationSet }); view.dispatch(tr); } export const ImagePluginKey = new PluginKey("imagePlugin"); export const createDropImagePlugin = (upload: UploadFn): Plugin => { const plugin: Plugin = new Plugin({ key: ImagePluginKey, state: { // Initialize the plugin state with an empty DecorationSet init: () => DecorationSet.empty, // Apply transactions to update the state apply: (tr, old) => { // Handle custom transaction steps that update decorations const meta = tr.getMeta(plugin); if (meta && meta.decorationSet) { return meta.decorationSet; } // Map decorations to the new document structure return old.map(tr.mapping, tr.doc); } }, props: { handleDOMEvents: { drop: (view, event) => { if (!event.dataTransfer?.files || event.dataTransfer?.files.length === 0) { return false; } event.preventDefault(); const files = Array.from(event.dataTransfer.files); const images = files.filter(file => /image/i.test(file.type)); if (images.length === 0) { console.log("No images found in dropped files"); return false; } images.forEach(image => { const position = view.posAtCoords({ left: event.clientX, top: event.clientY }); if (!position) return; const reader = new FileReader(); reader.onload = async (readerEvent) => { await onFileRead(view, readerEvent, position.pos, upload, image); }; reader.readAsDataURL(image); }); return true; } }, handlePaste(view, event, slice) { const items = Array.from(event.clipboardData?.items || []); const pos = view.state.selection.from; let anyImageFound = false; items.forEach((item) => { const image = item.getAsFile(); if (image && /image/i.test(item.type)) { anyImageFound = true; const reader = new FileReader(); reader.onload = async (readerEvent) => { await onFileRead(view, readerEvent, pos, upload, image); }; reader.readAsDataURL(image); } }); return anyImageFound; }, decorations(state) { return plugin.getState(state); } }, view(editorView) { // This is needed to immediately apply the decoration updates return { update(view, prevState) { const prevDecos = plugin.getState(prevState); const newDecos = plugin.getState(view.state); if (prevDecos !== newDecos) { view.updateState(view.state); } } }; } }); return plugin; };