feat: enhance editor uploads (#895)

* * multi-file paste support
* allow media files (image/videos) to be attachments
* insert trailing node if file placeholder is at the end of the editor

* fix video align
This commit is contained in:
Philip Okugbe
2025-03-15 18:27:26 +00:00
committed by GitHub
parent 573457403e
commit 21c3ad0ecc
8 changed files with 123 additions and 44 deletions

View File

@ -1,6 +1,10 @@
import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { MediaUploadOptions, UploadFn } from "../media-utils";
import {
insertTrailingNode,
MediaUploadOptions,
UploadFn,
} from "../media-utils";
import { IAttachment } from "../types";
const uploadKey = new PluginKey("attachment-upload");
@ -33,7 +37,8 @@ export const AttachmentUploadPlugin = ({
placeholder.appendChild(uploadingText);
const deco = Decoration.widget(pos + 1, placeholder, {
const realPos = pos + 1;
const deco = Decoration.widget(realPos, placeholder, {
id,
});
set = set.add(tr.doc, [deco]);
@ -64,8 +69,8 @@ function findPlaceholder(state: EditorState, id: {}) {
export const handleAttachmentUpload =
({ validateFn, onUpload }: MediaUploadOptions): UploadFn =>
async (file, view, pos, pageId) => {
const validated = validateFn?.(file);
async (file, view, pos, pageId, allowMedia) => {
const validated = validateFn?.(file, allowMedia);
// @ts-ignore
if (!validated) return;
// A fresh object to act as the ID for this upload
@ -82,6 +87,8 @@ export const handleAttachmentUpload =
fileName: file.name,
},
});
insertTrailingNode(tr, pos, view);
view.dispatch(tr);
await onUpload(file, pageId).then(

View File

@ -1,6 +1,6 @@
import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { MediaUploadOptions, UploadFn } from "../media-utils";
import { insertTrailingNode, MediaUploadOptions, UploadFn } from "../media-utils";
import { IAttachment } from "../types";
const uploadKey = new PluginKey("image-upload");
@ -69,13 +69,13 @@ export const handleImageUpload =
// A fresh object to act as the ID for this upload
const id = {};
// Replace the selection with a placeholder
const tr = view.state.tr;
if (!tr.selection.empty) tr.deleteSelection();
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const tr = view.state.tr;
// Replace the selection with a placeholder
if (!tr.selection.empty) tr.deleteSelection();
tr.setMeta(uploadKey, {
add: {
id,
@ -83,6 +83,8 @@ export const handleImageUpload =
src: reader.result,
},
});
insertTrailingNode(tr, pos, view);
view.dispatch(tr);
};

View File

@ -1,13 +1,29 @@
import type { EditorView } from "@tiptap/pm/view";
import { Transaction } from "@tiptap/pm/state";
export type UploadFn = (
file: File,
view: EditorView,
pos: number,
pageId: string,
// only applicable to file attachments
allowMedia?: boolean,
) => void;
export interface MediaUploadOptions {
validateFn?: (file: File) => void;
validateFn?: (file: File, allowMedia?: boolean) => void;
onUpload: (file: File, pageId: string) => Promise<any>;
}
export function insertTrailingNode(
tr: Transaction,
pos: number,
view: EditorView,
) {
// create trailing node after decoration
// if decoration is at the last node
const currentDocSize = view.state.doc.content.size;
if (pos + 1 === currentDocSize) {
tr.insert(currentDocSize, view.state.schema.nodes.paragraph.create());
}
}

View File

@ -1,6 +1,10 @@
import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { MediaUploadOptions, UploadFn } from "../media-utils";
import {
insertTrailingNode,
MediaUploadOptions,
UploadFn,
} from "../media-utils";
import { IAttachment } from "../types";
const uploadKey = new PluginKey("video-upload");
@ -70,12 +74,13 @@ export const handleVideoUpload =
const id = {};
// Replace the selection with a placeholder
const tr = view.state.tr;
if (!tr.selection.empty) tr.deleteSelection();
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const tr = view.state.tr;
if (!tr.selection.empty) tr.deleteSelection();
tr.setMeta(uploadKey, {
add: {
id,
@ -83,6 +88,8 @@ export const handleVideoUpload =
src: reader.result,
},
});
insertTrailingNode(tr, pos, view);
view.dispatch(tr);
};