Files
outline/shared/editor/lib/uploadPlaceholder.tsx

148 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}