Add support for uploads through drag and drop on the editor
authorAlexander Ebert <ebert@woltlab.com>
Mon, 29 Apr 2024 12:58:58 +0000 (14:58 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 8 Jun 2024 10:19:39 +0000 (12:19 +0200)
ts/WoltLabSuite/Core/Api/Files/Chunk/Chunk.ts
ts/WoltLabSuite/Core/Component/Attachment/List.ts
ts/WoltLabSuite/Core/Component/Ckeditor/Attachment.ts
ts/WoltLabSuite/Core/Component/File/Upload.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Attachment.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js

index 84dd41ea9f76946eed4c4371147ffe64f6f67a73..0bc71b9f63804845bc7affb7b1ff5f0d3c9aee62 100644 (file)
@@ -1,19 +1,20 @@
 import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
 import { ApiResult, apiResultFromError, apiResultFromValue } from "../../Result";
 
-export type Response =
-  | {
-      completed: false;
-    }
-  | {
-      completed: true;
-      generateThumbnails: boolean;
-      fileID: number;
-      typeName: string;
-      mimeType: string;
-      link: string;
-      data: Record<string, unknown>;
-    };
+export type ResponseIncomplete = {
+  completed: false;
+};
+export type ResponseCompleted = {
+  completed: true;
+  generateThumbnails: boolean;
+  fileID: number;
+  typeName: string;
+  mimeType: string;
+  link: string;
+  data: Record<string, unknown>;
+};
+
+export type Response = ResponseIncomplete | ResponseCompleted;
 
 export async function uploadChunk(
   identifier: string,
index 3920343d9dec1d6e8b65e19d9c01195058a52c01..1dd33f8c63a1a4de7f27b16100f04465cf4484c6 100644 (file)
@@ -1,8 +1,9 @@
 import { deleteFile } from "WoltLabSuite/Core/Api/Files/DeleteFile";
-import { dispatchToCkeditor } from "../Ckeditor/Event";
+import { dispatchToCkeditor, listenToCkeditor } from "../Ckeditor/Event";
 import WoltlabCoreFileElement from "../File/woltlab-core-file";
 
 import "../File/woltlab-core-file";
+import { CkeditorDropEvent } from "../File/Upload";
 
 type FileProcessorData = {
   attachmentID: number;
@@ -133,6 +134,12 @@ export function setup(editorId: string): void {
     return;
   }
 
+  const editor = document.getElementById(editorId);
+  if (editor === null) {
+    // TODO: error handling
+    return;
+  }
+
   const uploadButton = container.querySelector("woltlab-core-file-upload");
   if (uploadButton === null) {
     throw new Error("Expected the container to contain an upload button", {
@@ -153,6 +160,13 @@ export function setup(editorId: string): void {
     upload(fileList!, event.detail, editorId);
   });
 
+  listenToCkeditor(editor).uploadAttachment((payload) => {
+    const event = new CustomEvent<CkeditorDropEvent>("ckeditorDrop", {
+      detail: payload,
+    });
+    uploadButton.dispatchEvent(event);
+  });
+
   const existingFiles = container.querySelector<HTMLElement>(".attachment__list__existingFiles");
   if (existingFiles !== null) {
     existingFiles.querySelectorAll("woltlab-core-file").forEach((file) => {
index faee715f8703e6e334491ae672b9366d01683776..26af63fc5fec7cfd3f2e139e282aa8d9f53842c5 100644 (file)
@@ -19,7 +19,7 @@ type UploadResult = {
   };
 };
 
-type AttachmentData = {
+export type AttachmentData = {
   attachmentId: number;
   url: string;
 };
@@ -36,14 +36,16 @@ function uploadAttachment(element: HTMLElement, file: File, abortController?: Ab
   dispatchToCkeditor(element).uploadAttachment(payload);
 
   return new Promise<UploadResult>((resolve) => {
-    void payload.promise!.then(({ attachmentId, url }) => {
-      resolve({
-        "data-attachment-id": attachmentId.toString(),
-        urls: {
-          default: url,
-        },
-      });
-    });
+    void payload
+      .promise!.then(({ attachmentId, url }) => {
+        resolve({
+          "data-attachment-id": attachmentId.toString(),
+          urls: {
+            default: url,
+          },
+        });
+      })
+      .catch(() => {});
   });
 }
 
index 6d0eaad2e71ba448bd223c78ee1b5816d37197ee..d90246a556aeaf89ed792386095331323bcd1380 100644 (file)
@@ -1,9 +1,19 @@
 import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
 import { upload as filesUpload } from "WoltLabSuite/Core/Api/Files/Upload";
 import WoltlabCoreFileElement from "./woltlab-core-file";
-import { Response as UploadChunkResponse, uploadChunk } from "WoltLabSuite/Core/Api/Files/Chunk/Chunk";
+import {
+  ResponseCompleted,
+  Response as UploadChunkResponse,
+  uploadChunk,
+} from "WoltLabSuite/Core/Api/Files/Chunk/Chunk";
 import { generateThumbnails } from "WoltLabSuite/Core/Api/Files/GenerateThumbnails";
 import ImageResizer from "WoltLabSuite/Core/Image/Resizer";
+import { AttachmentData } from "../Ckeditor/Attachment";
+
+export type CkeditorDropEvent = {
+  file: File;
+  promise?: Promise<unknown>;
+};
 
 export type ThumbnailsGenerated = {
   data: GenerateThumbnailsResponse;
@@ -24,7 +34,7 @@ type ResizeConfiguration = {
   quality: number;
 };
 
-async function upload(element: WoltlabCoreFileUploadElement, file: File): Promise<void> {
+async function upload(element: WoltlabCoreFileUploadElement, file: File): Promise<ResponseCompleted | undefined> {
   const typeName = element.dataset.typeName!;
 
   const fileHash = await getSha256Hash(await file.arrayBuffer());
@@ -45,7 +55,7 @@ async function upload(element: WoltlabCoreFileUploadElement, file: File): Promis
     }
 
     console.log(validationError);
-    return;
+    return undefined;
   }
 
   const { identifier, numberOfChunks } = response.value;
@@ -69,6 +79,10 @@ async function upload(element: WoltlabCoreFileUploadElement, file: File): Promis
     }
 
     await chunkUploadCompleted(fileElement, response.value);
+
+    if (response.value.completed) {
+      return response.value;
+    }
   }
 }
 
@@ -189,5 +203,44 @@ export function setup(): void {
         void upload(element, resizedFile);
       });
     });
+
+    element.addEventListener("ckeditorDrop", (event: CustomEvent<CkeditorDropEvent>) => {
+      const { file } = event.detail;
+
+      let promiseResolve: (data: AttachmentData) => void;
+      let promiseReject: () => void;
+      event.detail.promise = new Promise<AttachmentData>((resolve, reject) => {
+        promiseResolve = resolve;
+        promiseReject = reject;
+      });
+
+      clearPreviousErrors(element);
+
+      if (!validateFile(element, file)) {
+        promiseReject!();
+
+        return;
+      }
+
+      void resizeImage(element, file).then(async (resizeFile) => {
+        try {
+          const data = await upload(element, resizeFile);
+          if (data === undefined || typeof data.data.attachmentID !== "number") {
+            promiseReject();
+          } else {
+            const attachmentData: AttachmentData = {
+              attachmentId: data.data.attachmentID,
+              url: data.link,
+            };
+
+            promiseResolve(attachmentData);
+          }
+        } catch (e) {
+          promiseReject();
+
+          throw e;
+        }
+      });
+    });
   });
 }
index 54e633c75cb5cc51eec797c3c0326f9263a599c2..4d8df96bb2e6f62f6d4e01c2c5e489ea3e990785 100644 (file)
@@ -94,6 +94,11 @@ define(["require", "exports", "WoltLabSuite/Core/Api/Files/DeleteFile", "../Cked
             // TODO: error handling
             return;
         }
+        const editor = document.getElementById(editorId);
+        if (editor === null) {
+            // TODO: error handling
+            return;
+        }
         const uploadButton = container.querySelector("woltlab-core-file-upload");
         if (uploadButton === null) {
             throw new Error("Expected the container to contain an upload button", {
@@ -111,6 +116,12 @@ define(["require", "exports", "WoltLabSuite/Core/Api/Files/DeleteFile", "../Cked
         uploadButton.addEventListener("uploadStart", (event) => {
             upload(fileList, event.detail, editorId);
         });
+        (0, Event_1.listenToCkeditor)(editor).uploadAttachment((payload) => {
+            const event = new CustomEvent("ckeditorDrop", {
+                detail: payload,
+            });
+            uploadButton.dispatchEvent(event);
+        });
         const existingFiles = container.querySelector(".attachment__list__existingFiles");
         if (existingFiles !== null) {
             existingFiles.querySelectorAll("woltlab-core-file").forEach((file) => {
index 9a9b43b4ff9e3f3b35816157e1efad85dcdc27b0..1b45f0b1f1bd3bc9df68a51f7d1462924249ceed 100644 (file)
@@ -15,14 +15,16 @@ define(["require", "exports", "./Event"], function (require, exports, Event_1) {
         const payload = { abortController, file };
         (0, Event_1.dispatchToCkeditor)(element).uploadAttachment(payload);
         return new Promise((resolve) => {
-            void payload.promise.then(({ attachmentId, url }) => {
+            void payload
+                .promise.then(({ attachmentId, url }) => {
                 resolve({
                     "data-attachment-id": attachmentId.toString(),
                     urls: {
                         default: url,
                     },
                 });
-            });
+            })
+                .catch(() => { });
         });
     }
     function setupInsertAttachment(ckeditor) {
index acd60e9ca4e65c73244e473eb7b88c677daf5fae..d3baeb81a640900aaff485e01891ffe23390bc19 100644 (file)
@@ -18,7 +18,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
                 throw response.error;
             }
             console.log(validationError);
-            return;
+            return undefined;
         }
         const { identifier, numberOfChunks } = response.value;
         const chunkSize = Math.ceil(file.size / numberOfChunks);
@@ -34,6 +34,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
                 throw response.error;
             }
             await chunkUploadCompleted(fileElement, response.value);
+            if (response.value.completed) {
+                return response.value;
+            }
         }
     }
     async function chunkUploadCompleted(fileElement, result) {
@@ -127,6 +130,39 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
                     void upload(element, resizedFile);
                 });
             });
+            element.addEventListener("ckeditorDrop", (event) => {
+                const { file } = event.detail;
+                let promiseResolve;
+                let promiseReject;
+                event.detail.promise = new Promise((resolve, reject) => {
+                    promiseResolve = resolve;
+                    promiseReject = reject;
+                });
+                clearPreviousErrors(element);
+                if (!validateFile(element, file)) {
+                    promiseReject();
+                    return;
+                }
+                void resizeImage(element, file).then(async (resizeFile) => {
+                    try {
+                        const data = await upload(element, resizeFile);
+                        if (data === undefined || typeof data.data.attachmentID !== "number") {
+                            promiseReject();
+                        }
+                        else {
+                            const attachmentData = {
+                                attachmentId: data.data.attachmentID,
+                                url: data.link,
+                            };
+                            promiseResolve(attachmentData);
+                        }
+                    }
+                    catch (e) {
+                        promiseReject();
+                        throw e;
+                    }
+                });
+            });
         });
     }
     exports.setup = setup;