From 5cc329c2f1f2f9414aeaa4239c6c102db8d84fd5 Mon Sep 17 00:00:00 2001 From: thiloho <123883702+thiloho@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:31:41 +0200 Subject: [PATCH] Enable image pasting in markdown textarea --- web-app/src/lib/utils.ts | 33 +++++++++++++++++++ .../website/[websiteId]/+page.server.ts | 25 ++++++++++++++ .../website/[websiteId]/+page.svelte | 8 ++++- .../articles/[articleId]/+page.server.ts | 27 ++++++++++++++- .../articles/[articleId]/+page.svelte | 9 ++++- 5 files changed, 99 insertions(+), 3 deletions(-) diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index d4110f7..ed917e0 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -113,5 +113,38 @@ export const md = markdownit({ md.core.ruler.push("header_sections", addSections); }); +export const handleImagePaste = async (event: ClipboardEvent) => { + const clipboardItems = Array.from(event.clipboardData?.items || []); + const file = clipboardItems.find((item) => item.type.startsWith("image/")); + + if (!file) return; + + event.preventDefault(); + + const fileObject = file.getAsFile(); + + if (!fileObject) return; + + const formData = new FormData(); + formData.append("file", fileObject); + + const request = await fetch("?/pasteImage", { + method: "POST", + body: formData + }); + + const response = await request.json(); + const fileId = JSON.parse(response.data)[1]; + const fileUrl = `${API_BASE_PREFIX}/rpc/retrieve_file?id=${fileId}`; + + const target = event.target as HTMLTextAreaElement; + const newContent = + target.value.slice(0, target.selectionStart) + + `![](${fileUrl})` + + target.value.slice(target.selectionStart); + + return newContent; +}; + export const API_BASE_PREFIX = dev ? "http://localhost:3000" : "/api"; export const NGINX_BASE_PREFIX = dev ? "http://localhost:18000" : ""; diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts index e9343e0..9da5bbf 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts @@ -180,5 +180,30 @@ export const actions: Actions = { success: true, message: "Successfully updated footer" }; + }, + pasteImage: async ({ request, fetch, cookies, params }) => { + const data = await request.formData(); + const file = data.get("file") as File; + + const fileData = await fetch(`${API_BASE_PREFIX}/rpc/upload_file`, { + method: "POST", + headers: { + "Content-Type": "application/octet-stream", + Authorization: `Bearer ${cookies.get("session_token")}`, + Accept: "application/vnd.pgrst.object+json", + "X-Website-Id": params.websiteId, + "X-Mimetype": file.type, + "X-Original-Filename": file.name + }, + body: await file.arrayBuffer() + }); + + const fileJSON = await fileData.json(); + + if (!fileData.ok) { + return { success: false, message: fileJSON.message }; + } + + return { fileId: fileJSON.file_id }; } }; diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/+page.svelte b/web-app/src/routes/(authenticated)/website/[websiteId]/+page.svelte index f988edd..de55ef1 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/+page.svelte +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/+page.svelte @@ -1,7 +1,7 @@ @@ -149,6 +154,7 @@ bind:value={previewContent} bind:this={mainContentTextarea} onscroll={updateScrollPercentage} + onpaste={handlePaste} required>{data.home.main_content} diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.server.ts index 6bb8fef..e5efe15 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.server.ts +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.server.ts @@ -18,7 +18,7 @@ export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) = }; export const actions: Actions = { - default: async ({ fetch, cookies, request, params }) => { + editArticle: async ({ fetch, cookies, request, params }) => { const data = await request.formData(); const coverFile = data.get("cover-image") as File; @@ -63,5 +63,30 @@ export const actions: Actions = { } return { success: true, message: "Successfully updated article" }; + }, + pasteImage: async ({ request, fetch, cookies, params }) => { + const data = await request.formData(); + const file = data.get("file") as File; + + const fileData = await fetch(`${API_BASE_PREFIX}/rpc/upload_file`, { + method: "POST", + headers: { + "Content-Type": "application/octet-stream", + Authorization: `Bearer ${cookies.get("session_token")}`, + Accept: "application/vnd.pgrst.object+json", + "X-Website-Id": params.websiteId, + "X-Mimetype": file.type, + "X-Original-Filename": file.name + }, + body: await file.arrayBuffer() + }); + + const fileJSON = await fileData.json(); + + if (!fileData.ok) { + return { success: false, message: fileJSON.message }; + } + + return { fileId: fileJSON.file_id }; } }; diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.svelte b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.svelte index ebf3a9d..f6aff4f 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.svelte +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.svelte @@ -5,7 +5,7 @@ import SuccessOrError from "$lib/components/SuccessOrError.svelte"; import type { ActionData, PageServerData } from "./$types"; import Modal from "$lib/components/Modal.svelte"; - import { API_BASE_PREFIX } from "$lib/utils"; + import { API_BASE_PREFIX, handleImagePaste } from "$lib/utils"; const { data, form } = $props<{ data: PageServerData; form: ActionData }>(); @@ -17,6 +17,11 @@ const { scrollTop, scrollHeight, clientHeight } = mainContentTextarea; textareaScrollTop = (scrollTop / (scrollHeight - clientHeight)) * 100; }; + + const handlePaste = async (event: ClipboardEvent) => { + const newContent = await handleImagePaste(event); + previewContent = newContent; + }; @@ -33,6 +38,7 @@
{ return async ({ update }) => { @@ -94,6 +100,7 @@ bind:value={previewContent} bind:this={mainContentTextarea} onscroll={updateScrollPercentage} + onpaste={handlePaste} required>{data.article.main_content}