mirror of
https://github.com/docmost/docmost.git
synced 2026-05-19 16:04:17 +08:00
106 lines
3.1 KiB
TypeScript
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 };
|