Add image upload to Markdown editor: paste, drag-drop, toolbar button
Deploy to LXC / deploy (push) Successful in 22s
Deploy to LXC / deploy (push) Successful in 22s
- /api/upload-image endpoint saves images and returns URL - Paste images from clipboard directly into the editor - Drag and drop images onto the editor - Toolbar image button opens file picker for manual upload - Uploaded images inserted as  Markdown at cursor position Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,37 @@
|
||||
let textareaEl: HTMLTextAreaElement;
|
||||
let editor: any = null;
|
||||
|
||||
async function uploadImage(file: File): Promise<string | null> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload-image', { method: 'POST', body: formData });
|
||||
if (!res.ok) throw new Error('Upload failed');
|
||||
const { url } = await res.json();
|
||||
return url;
|
||||
} catch {
|
||||
alert('Image upload failed');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function insertImageAtCursor(url: string, altText: string) {
|
||||
if (!editor) return;
|
||||
const cm = editor.codemirror;
|
||||
const pos = cm.getCursor();
|
||||
cm.replaceRange(`\n`, pos);
|
||||
value = editor.value();
|
||||
}
|
||||
|
||||
async function handleFiles(files: FileList | File[]) {
|
||||
for (const file of files) {
|
||||
if (!file.type.startsWith('image/')) continue;
|
||||
const url = await uploadImage(file);
|
||||
if (url) insertImageAtCursor(url, file.name.replace(/\.[^.]+$/, ''));
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!browser) return;
|
||||
|
||||
@@ -25,12 +56,28 @@
|
||||
spellChecker: false,
|
||||
autofocus: false,
|
||||
placeholder,
|
||||
status: false,
|
||||
status: ['upload'],
|
||||
minHeight: '300px',
|
||||
toolbar: [
|
||||
'bold', 'italic', 'heading', '|',
|
||||
'quote', 'unordered-list', 'ordered-list', '|',
|
||||
'link', 'image', 'table', 'code', '|',
|
||||
'link',
|
||||
{
|
||||
name: 'upload-image',
|
||||
action: () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.multiple = true;
|
||||
input.onchange = () => {
|
||||
if (input.files) handleFiles(input.files);
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
className: 'fa fa-image',
|
||||
title: 'Upload Image'
|
||||
},
|
||||
'table', 'code', '|',
|
||||
'preview', 'side-by-side', 'fullscreen', '|',
|
||||
'guide'
|
||||
],
|
||||
@@ -40,6 +87,32 @@
|
||||
editor.codemirror.on('change', () => {
|
||||
value = editor.value();
|
||||
});
|
||||
|
||||
// Drag and drop images
|
||||
const cm = editor.codemirror;
|
||||
cm.on('drop', (_: any, e: DragEvent) => {
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
e.preventDefault();
|
||||
handleFiles(e.dataTransfer.files);
|
||||
}
|
||||
});
|
||||
|
||||
// Paste images
|
||||
cm.on('paste', (_: any, e: ClipboardEvent) => {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items) return;
|
||||
const imageFiles: File[] = [];
|
||||
for (const item of items) {
|
||||
if (item.type.startsWith('image/')) {
|
||||
const file = item.getAsFile();
|
||||
if (file) imageFiles.push(file);
|
||||
}
|
||||
}
|
||||
if (imageFiles.length > 0) {
|
||||
e.preventDefault();
|
||||
handleFiles(imageFiles);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -80,6 +153,9 @@
|
||||
.markdown-editor :global(.editor-toolbar button.active) {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
.markdown-editor :global(.editor-statusbar) {
|
||||
display: none;
|
||||
}
|
||||
/* Dark mode */
|
||||
:global(.dark) .markdown-editor :global(.CodeMirror) {
|
||||
background: #374151;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { saveImage } from '$lib/server/uploads.js';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
if (!locals.user) error(401, 'Not authenticated');
|
||||
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
if (!file || file.size === 0) {
|
||||
error(400, 'No file provided');
|
||||
}
|
||||
|
||||
try {
|
||||
const { filePath } = await saveImage(file, 'devices');
|
||||
return json({ url: filePath });
|
||||
} catch (err) {
|
||||
error(400, err instanceof Error ? err.message : 'Upload failed');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user