Files
docmost/packages/editor-ext/src/lib/image/image-upload.ts
T

106 lines
3.1 KiB
TypeScript

import { imageDimensionsFromData } from "image-dimensions";
import { MediaUploadOptions, UploadFn } from "../media-utils";
import { IAttachment } from "../types";
import { generateNodeId } from "../utils";
import { Node } from "@tiptap/pm/model";
const findImageNodeByPlaceholderId = (
doc: Node,
placeholderId: string,
): { node: Node; pos: number } | null => {
let result: { node: Node; pos: number } | null = null;
doc.descendants((node, pos) => {
if (result) return false;
if (
node.type.name === "image" &&
node.attrs.placeholderId === placeholderId
) {
result = { node, pos };
return false;
}
return true;
});
return result;
};
const handleImageUpload =
({ validateFn, onUpload }: MediaUploadOptions): UploadFn =>
async (file, view, pos, pageId) => {
// check if the file is an image
const validated = validateFn?.(file);
// @ts-ignore
if (!validated) return;
const imageDimensions = imageDimensionsFromData(await file.bytes());
const placeholderId = generateNodeId();
const aspectRatio = imageDimensions
? imageDimensions.width / imageDimensions.height
: undefined;
const initialPlaceholderNode = view.state.schema.nodes.image?.create({
placeholderId,
aspectRatio,
});
let placeholderShown = false;
let tr = view.state.tr;
if (!initialPlaceholderNode) return;
const { parent } = tr.doc.resolve(pos);
const isEmptyTextBlock = parent.isTextblock && !parent.childCount;
if (isEmptyTextBlock) {
// Replace e.g. empty paragraph with the image
tr.replaceRangeWith(pos - 1, pos + 1, initialPlaceholderNode);
} else {
tr.insert(pos, initialPlaceholderNode);
}
// Only show the placeholder if the upload takes more than 250ms
const displayPlaceholderTimeout = setTimeout(() => {
view.dispatch(tr);
placeholderShown = true;
tr = view.state.tr;
}, 250);
try {
const attachment: IAttachment = await onUpload(file, pageId);
const { pos: currentPos = null } =
findImageNodeByPlaceholderId(tr.doc, placeholderId) || {};
// If the placeholder is not found or attachment is missing, abort the process
if (currentPos === null || !attachment) return;
// Update the placeholder node with the actual image data
tr.setNodeMarkup(currentPos, undefined, {
src: `/api/files/${attachment.id}/${attachment.fileName}`,
attachmentId: attachment.id,
title: attachment.fileName,
size: attachment.fileSize,
aspectRatio,
});
} catch (error) {
const { pos: currentPos = null } =
findImageNodeByPlaceholderId(tr.doc, placeholderId) || {};
if (currentPos === null) return;
// Delete the image placeholder on error
tr.delete(currentPos, currentPos + 2);
} finally {
clearTimeout(displayPlaceholderTimeout);
// If the placeholder was shown, delay showing the image to avoid flicker
if (placeholderShown) {
setTimeout(() => {
view.dispatch(tr);
}, 100);
} else {
view.dispatch(tr);
}
}
};
export { handleImageUpload };