148 lines
4.8 KiB
TypeScript
148 lines
4.8 KiB
TypeScript
import { EditorState, Plugin } from "prosemirror-state";
|
||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||
import * as React from "react";
|
||
import ReactDOM from "react-dom";
|
||
import FileExtension from "../components/FileExtension";
|
||
import { isRemoteTransaction } from "./multiplayer";
|
||
import { recreateTransform } from "./prosemirror-recreate-transform";
|
||
|
||
// based on the example at: https://prosemirror.net/examples/upload/
|
||
const uploadPlaceholder = new Plugin({
|
||
state: {
|
||
init() {
|
||
return DecorationSet.empty;
|
||
},
|
||
apply(tr, set: DecorationSet) {
|
||
let mapping = tr.mapping;
|
||
|
||
// See if the transaction adds or removes any placeholders – the placeholder display is
|
||
// different depending on if we're uploading an image, video or plain file
|
||
const action = tr.getMeta(this);
|
||
const hasDecorations = set.find().length;
|
||
|
||
// Note: We always rebuild the mapping if the transaction comes from this plugin as otherwise
|
||
// with the default mapping decorations are wiped out when you upload multiple files at a time.
|
||
if (hasDecorations && (isRemoteTransaction(tr) || action)) {
|
||
try {
|
||
mapping = recreateTransform(tr.before, tr.doc, {
|
||
complexSteps: true,
|
||
wordDiffs: false,
|
||
simplifyDiff: true,
|
||
}).mapping;
|
||
} catch (err) {
|
||
// eslint-disable-next-line no-console
|
||
console.warn("Failed to recreate transform: ", err);
|
||
}
|
||
}
|
||
|
||
set = set.map(mapping, tr.doc);
|
||
|
||
if (action?.add) {
|
||
if (action.add.replaceExisting) {
|
||
const $pos = tr.doc.resolve(action.add.pos);
|
||
const nodeAfter = $pos.nodeAfter;
|
||
if (!nodeAfter) {
|
||
return;
|
||
}
|
||
|
||
const deco = Decoration.node(
|
||
$pos.pos,
|
||
$pos.pos + nodeAfter.nodeSize,
|
||
{
|
||
class: `${nodeAfter.type.name}-replacement-uploading`,
|
||
},
|
||
{
|
||
id: action.add.id,
|
||
}
|
||
);
|
||
set = set.add(tr.doc, [deco]);
|
||
} else if (action.add.isImage) {
|
||
const element = document.createElement("div");
|
||
element.className = "image placeholder";
|
||
|
||
const img = document.createElement("img");
|
||
img.src = URL.createObjectURL(action.add.file);
|
||
img.style.width = `${action.add.dimensions?.width}px`;
|
||
|
||
element.appendChild(img);
|
||
|
||
const deco = Decoration.widget(action.add.pos, element, {
|
||
id: action.add.id,
|
||
});
|
||
set = set.add(tr.doc, [deco]);
|
||
} else if (action.add.isVideo) {
|
||
const element = document.createElement("div");
|
||
element.className = "video placeholder";
|
||
|
||
const video = document.createElement("video");
|
||
video.src = URL.createObjectURL(action.add.file);
|
||
video.autoplay = false;
|
||
video.controls = false;
|
||
video.width = action.add.dimensions?.width;
|
||
video.height = action.add.dimensions?.height;
|
||
|
||
element.appendChild(video);
|
||
|
||
const deco = Decoration.widget(action.add.pos, element, {
|
||
id: action.add.id,
|
||
});
|
||
set = set.add(tr.doc, [deco]);
|
||
} else {
|
||
const element = document.createElement("div");
|
||
element.className = "file placeholder";
|
||
|
||
const icon = document.createElement("div");
|
||
const title = document.createElement("div");
|
||
title.className = "title";
|
||
title.innerText = action.add.file.name;
|
||
|
||
const subtitle = document.createElement("div");
|
||
subtitle.className = "subtitle";
|
||
subtitle.innerText = "Uploading…";
|
||
|
||
ReactDOM.render(<FileExtension title={action.add.file.name} />, icon);
|
||
|
||
element.appendChild(icon);
|
||
element.appendChild(title);
|
||
element.appendChild(subtitle);
|
||
|
||
const deco = Decoration.widget(action.add.pos, element, {
|
||
id: action.add.id,
|
||
});
|
||
set = set.add(tr.doc, [deco]);
|
||
}
|
||
}
|
||
|
||
if (action?.remove) {
|
||
set = set.remove(
|
||
set.find(undefined, undefined, (spec) => spec.id === action.remove.id)
|
||
);
|
||
}
|
||
return set;
|
||
},
|
||
},
|
||
props: {
|
||
decorations(state) {
|
||
return this.getState(state);
|
||
},
|
||
},
|
||
});
|
||
|
||
export default uploadPlaceholder;
|
||
|
||
/**
|
||
* Find the position of a placeholder by its ID
|
||
*
|
||
* @param state The editor state
|
||
* @param id The placeholder ID
|
||
* @returns The placeholder position as a tuple of [from, to] or null if not found
|
||
*/
|
||
export function findPlaceholder(
|
||
state: EditorState,
|
||
id: string
|
||
): [number, number] | null {
|
||
const decos = uploadPlaceholder.getState(state);
|
||
const found = decos?.find(undefined, undefined, (spec) => spec.id === id);
|
||
return found?.length ? [found[0].from, found[0].to] : null;
|
||
}
|