@firecms/core
Version:
Awesome Firebase/Firestore-based headless open-source CMS
134 lines (114 loc) • 5.06 kB
text/typescript
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;
};