From 55047b5d90dc89d33791df7213bdacfb3f5b5bb9 Mon Sep 17 00:00:00 2001 From: Timothy Cardoza Date: Thu, 12 Mar 2026 18:09:45 -0500 Subject: [PATCH] Copy and paste as well as dragging files added --- src/components/TaskEditor.tsx | 70 ++++++++++++++++++++++++++++++++++- src/lib/extensions/image.ts | 17 ++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/components/TaskEditor.tsx b/src/components/TaskEditor.tsx index 7ab0c6a..cbf2e2d 100644 --- a/src/components/TaskEditor.tsx +++ b/src/components/TaskEditor.tsx @@ -8,7 +8,10 @@ import { CustomImage as Image } from "@/lib/extensions/image"; import Underline from "@tiptap/extension-underline"; import { X } from "lucide-solid"; import { cn } from "@/lib/utils"; -import { store } from "@/store"; +import { store, activeTaskId, activeNoteId, uploadTaskAttachment, uploadNoteAttachment } from "@/store"; +import { convertImageToWebpFile } from "@/lib/utils"; +import { toast } from "solid-sonner"; +import type { Editor } from "@tiptap/core"; import { SlashCommands } from "@/lib/slash-command"; import { suggestion } from "@/lib/slash-renderer"; @@ -57,6 +60,50 @@ export const TaskEditor: Component = (props) => { setZoomOffset({ x: 0, y: 0 }); }; + const handleFileUploads = async (editor: Editor, files: File[]) => { + const taskId = activeTaskId(); + const noteId = activeNoteId(); + + if (!taskId && !noteId) { + toast.error("Cannot upload files outside of a task or note."); + return; + } + + for (const file of files) { + const isImage = file.type.startsWith('image/'); + const isVideo = file.type.startsWith('video/'); + + toast.promise( + (async () => { + let fileToUpload = file; + if (isImage) { + fileToUpload = await convertImageToWebpFile(file); + } + + if (taskId) { + return uploadTaskAttachment(taskId, fileToUpload); + } else { + return uploadNoteAttachment(noteId!, fileToUpload); + } + })(), + { + loading: `Uploading ${file.name}...`, + success: ({ url, filename }) => { + if (isImage) { + editor.chain().focus().setImage({ src: url, filename }).run(); + } else if (isVideo) { + editor.chain().focus().setVideo({ src: url, filename }).run(); + } else { + editor.chain().focus().insertFileAttachment({ src: url, filename, filesize: file.size.toString() }).run(); + } + return `${file.name} uploaded.`; + }, + error: `Failed to upload ${file.name}.` + } + ); + } + }; + const editor = createTiptapEditor(() => ({ element: editorRef!, extensions: [ @@ -179,6 +226,27 @@ export const TaskEditor: Component = (props) => { props.onUpdate(editor.getHTML()); }, editorProps: { + handlePaste: (_view, event) => { + const items = Array.from(event.clipboardData?.items || []); + const files = items + .map(item => item.getAsFile()) + .filter((file): file is File => file !== null); + + if (files.length > 0) { + event.preventDefault(); + handleFileUploads(editor()!, files); + return true; + } + return false; + }, + handleDrop: (_view, event, _slice, moved) => { + if (!moved && event.dataTransfer?.files?.length) { + event.preventDefault(); + handleFileUploads(editor()!, Array.from(event.dataTransfer.files)); + return true; + } + return false; + }, attributes: { class: cn( "prose prose-sm dark:prose-invert max-w-none focus:outline-none min-h-[150px]", diff --git a/src/lib/extensions/image.ts b/src/lib/extensions/image.ts index 59aeb3e..5618ef8 100644 --- a/src/lib/extensions/image.ts +++ b/src/lib/extensions/image.ts @@ -24,6 +24,17 @@ export const CustomImage = Image.extend({ }; }, + addCommands() { + return { + setImage: (options: { src: string, alt?: string, title?: string, filename?: string }) => ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: options, + }); + }, + }; + }, + addNodeView() { return ({ node, getPos, editor }) => { const wrapper = document.createElement('div'); @@ -76,10 +87,14 @@ export const CustomImage = Image.extend({ }, }); +import type { SetImageOptions } from "@tiptap/extension-image"; + declare module '@tiptap/core' { interface Commands { image: { - setImage: (options: { src: string, alt?: string, title?: string, filename?: string }) => ReturnType; + // @ts-ignore + setImage: (options: SetImageOptions & { filename?: string }) => ReturnType; }; } } +// @ts-ignore