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

@ -17,8 +17,11 @@ export const uploadAttachmentAction = handleAttachmentUpload({
throw err;
}
},
validateFn: (file) => {
if (file.type.includes("image/") || file.type.includes("video/")) {
validateFn: (file, allowMedia: boolean) => {
if (
(file.type.includes("image/") || file.type.includes("video/")) &&
!allowMedia
) {
return false;
}
if (file.size > getFileUploadSizeLimit()) {

View File

@ -40,10 +40,8 @@ export const handlePaste = (
if (event.clipboardData?.files.length) {
event.preventDefault();
const [file] = Array.from(event.clipboardData.files);
const pos = view.state.selection.from;
if (file) {
for (const file of event.clipboardData.files) {
const pos = view.state.selection.from;
uploadImageAction(file, view, pos, pageId);
uploadVideoAction(file, view, pos, pageId);
uploadAttachmentAction(file, view, pos, pageId);
@ -61,13 +59,13 @@ export const handleFileDrop = (
) => {
if (!moved && event.dataTransfer?.files.length) {
event.preventDefault();
const [file] = Array.from(event.dataTransfer.files);
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
// here we deduct 1 from the pos or else the image will create an extra node
if (file) {
for (const file of event.dataTransfer.files) {
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
uploadImageAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
uploadVideoAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
uploadAttachmentAction(file, view, coordinates?.pos ?? 0 - 1, pageId);

View File

@ -38,7 +38,8 @@ import {
LoomIcon,
MiroIcon,
TypeformIcon,
VimeoIcon, YoutubeIcon
VimeoIcon,
YoutubeIcon,
} from "@/components/icons";
const CommandGroups: SlashMenuGroupedItemsType = {
@ -221,13 +222,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
if (file.type.includes("image/*")) {
uploadImageAction(file, editor.view, pos, pageId);
} else if (file.type.includes("video/*")) {
uploadVideoAction(file, editor.view, pos, pageId);
} else {
uploadAttachmentAction(file, editor.view, pos, pageId);
}
uploadAttachmentAction(file, editor.view, pos, pageId, true);
}
};
input.click();
@ -368,7 +363,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["airtable"],
icon: AirtableIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'airtable' }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "airtable" })
.run();
},
},
{
@ -377,7 +377,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["loom"],
icon: LoomIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'loom' }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "loom" })
.run();
},
},
{
@ -386,7 +391,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["figma"],
icon: FigmaIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'figma' }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "figma" })
.run();
},
},
{
@ -395,7 +405,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["typeform"],
icon: TypeformIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'typeform' }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "typeform" })
.run();
},
},
{
@ -404,7 +419,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["miro"],
icon: MiroIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'miro' }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "miro" })
.run();
},
},
{
@ -413,7 +433,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["youtube", "yt"],
icon: YoutubeIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'youtube' }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "youtube" })
.run();
},
},
{
@ -422,7 +447,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["vimeo"],
icon: VimeoIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'vimeo' }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "vimeo" })
.run();
},
},
{
@ -431,7 +461,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["framer"],
icon: FramerIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'framer' }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "framer" })
.run();
},
},
{
@ -440,7 +475,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["google drive", "gdrive"],
icon: GoogleDriveIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'gdrive' }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "gdrive" })
.run();
},
},
{
@ -449,7 +489,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["google sheets", "gsheets"],
icon: GoogleSheetsIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'gsheets' }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setEmbed({ provider: "gsheets" })
.run();
},
},
],

View File

@ -22,6 +22,7 @@ export default function VideoView(props: NodeViewProps) {
controls
src={getFileUrl(src)}
className={clsx(selected ? "ProseMirror-selectednode" : "", alignClass)}
style={{ display: "block" }}
/>
</NodeViewWrapper>
);