Add `simpleReplace` and `hideDeleteButton` to the `FileProcessorFormField`
authorCyperghost <olaf_schmitz_1@t-online.de>
Thu, 12 Dec 2024 13:54:04 +0000 (14:54 +0100)
committerCyperghost <olaf_schmitz_1@t-online.de>
Wed, 18 Dec 2024 08:07:04 +0000 (09:07 +0100)
com.woltlab.wcf/templates/shared_fileProcessorFormField.tpl
ts/WoltLabSuite/Core/Component/File/Upload.ts
ts/WoltLabSuite/Core/Form/Builder/Field/Controller/FileProcessor.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/FileProcessor.js
wcfsetup/install/files/lib/system/form/builder/field/FileProcessorFormField.class.php

index b4dba63d9d869bed08d07da8fed385f641d8d3d7..8757b4b10e60469db405e8dd00dbd6989052df41 100644 (file)
@@ -24,6 +24,8 @@
                        '{unsafe:$field->getPrefixedId()|encodeJS}',
                        {if $field->isSingleFileUpload()}true{else}false{/if},
                        {if $field->isBigPreview()}true{else}false{/if},
+                       {if $field->isSimpleReplace()}true{else}false{/if},
+                       {if $field->isHideDeleteButton()}true{else}false{/if},
                        [{implode from=$actionButtons item=actionButton}{
                                title: '{unsafe:$actionButton['title']|encodeJS}',
                                icon: {if $actionButton['icon'] === null}undefined{else}'{unsafe:$actionButton['icon']->toHtml()|encodeJS}'{/if},
index 857bec5eeb737013e0f4902d4c8a0f9cdc581074..18b67b91cce77783e7cfb5b203d34868bf563eb6 100644 (file)
@@ -277,6 +277,8 @@ export function setup(): void {
             void upload(element, resizedFile);
           })
           .catch((e) => {
+            element.dispatchEvent(new CustomEvent("cancel"));
+
             if (e === undefined) {
               // User closed the dialog.
               return;
index 039c734c971300305dbc0aa6993f2fa5631222f5..e281dffa37b0baec3da3862f51e0ec1968379197 100644 (file)
@@ -35,6 +35,8 @@ export class FileProcessor {
   readonly #fileInput: HTMLInputElement;
   readonly #useBigPreview: boolean;
   readonly #singleFileUpload: boolean;
+  readonly #simpleReplace: boolean;
+  readonly #hideDeleteButton: boolean;
   readonly #extraButtons: ExtraButton[];
   #uploadResolve: undefined | (() => void);
 
@@ -42,11 +44,15 @@ export class FileProcessor {
     fieldId: string,
     singleFileUpload: boolean = false,
     useBigPreview: boolean = false,
+    simpleReplace: boolean = false,
+    hideDeleteButton: boolean = false,
     extraButtons: ExtraButton[] = [],
   ) {
     this.#fieldId = fieldId;
     this.#useBigPreview = useBigPreview;
     this.#singleFileUpload = singleFileUpload;
+    this.#simpleReplace = simpleReplace;
+    this.#hideDeleteButton = hideDeleteButton;
     this.#extraButtons = extraButtons;
 
     this.#container = document.getElementById(fieldId + "Container")!;
@@ -55,6 +61,18 @@ export class FileProcessor {
     }
 
     this.#uploadButton = this.#container.querySelector("woltlab-core-file-upload") as WoltlabCoreFileUploadElement;
+
+    if (this.#simpleReplace) {
+      this.#uploadButton.addEventListener("shouldUpload", () => {
+        const file = this.#uploadButton.parentElement!.querySelector("woltlab-core-file");
+        if (!file) {
+          return;
+        }
+
+        this.#simpleFileReplace(file);
+      });
+    }
+
     this.#uploadButton.addEventListener("uploadStart", (event: CustomEvent<WoltlabCoreFileElement>) => {
       if (this.#uploadResolve !== undefined) {
         this.#uploadResolve();
@@ -80,12 +98,14 @@ export class FileProcessor {
     buttons.classList.add("buttonList");
     buttons.classList.add(this.classPrefix + "item__buttons");
 
-    let listItem = document.createElement("li");
-    listItem.append(this.getDeleteButton(element));
-    buttons.append(listItem);
+    if (!this.#hideDeleteButton) {
+      const listItem = document.createElement("li");
+      listItem.append(this.getDeleteButton(element));
+      buttons.append(listItem);
+    }
 
-    if (this.#singleFileUpload) {
-      listItem = document.createElement("li");
+    if (this.#singleFileUpload && !this.#simpleReplace) {
+      const listItem = document.createElement("li");
       listItem.append(this.getReplaceButton(element));
       buttons.append(listItem);
     }
@@ -118,6 +138,45 @@ export class FileProcessor {
     container.append(buttons);
   }
 
+  protected getReplaceButton(element: WoltlabCoreFileElement): HTMLButtonElement {
+    const replaceButton = document.createElement("button");
+    replaceButton.type = "button";
+    replaceButton.classList.add("button", "small");
+    replaceButton.textContent = getPhrase("wcf.global.button.replace");
+    replaceButton.addEventListener("click", () => {
+      const oldContext = this.#startReplaceFile(element);
+
+      clearPreviousErrors(this.#uploadButton);
+
+      const changeEventListener = () => {
+        this.#fileInput.removeEventListener("cancel", cancelEventListener);
+
+        // Wait until the upload starts,
+        // the request to the server is not synchronized with the end of the `change` event.
+        // Otherwise, we would swap the context too soon.
+        void new Promise<void>((resolve) => {
+          this.#uploadResolve = resolve;
+        }).then(() => {
+          this.#uploadResolve = undefined;
+          this.#uploadButton.dataset.context = oldContext;
+        });
+      };
+      const cancelEventListener = () => {
+        this.#uploadButton.dataset.context = oldContext;
+        this.#registerFile(this.#replaceElement!);
+        this.#replaceElement = undefined;
+        this.#fileInput.removeEventListener("change", changeEventListener);
+      };
+
+      this.#fileInput.addEventListener("cancel", cancelEventListener, { once: true });
+      this.#fileInput.addEventListener("change", changeEventListener, { once: true });
+
+      this.#fileInput.click();
+    });
+
+    return replaceButton;
+  }
+
   #markElementUploadHasFailed(container: HTMLElement, element: WoltlabCoreFileElement, reason: unknown): void {
     fileInitializationFailed(container, element, reason);
 
@@ -150,51 +209,39 @@ export class FileProcessor {
     return deleteButton;
   }
 
-  protected getReplaceButton(element: WoltlabCoreFileElement): HTMLButtonElement {
-    const replaceButton = document.createElement("button");
-    replaceButton.type = "button";
-    replaceButton.classList.add("button", "small");
-    replaceButton.textContent = getPhrase("wcf.global.button.replace");
-    replaceButton.addEventListener("click", () => {
-      // Add to context an extra attribute that the replace button is clicked.
-      // After the dialog is closed or the file is selected, the context will be reset to his old value.
-      // This is necessary as the serverside validation will otherwise fail.
-      const oldContext = this.#uploadButton.dataset.context!;
-      const context = JSON.parse(oldContext);
-      context.__replace = true;
-      this.#uploadButton.dataset.context = JSON.stringify(context);
+  #simpleFileReplace(oldFile: WoltlabCoreFileElement) {
+    const oldContext = this.#startReplaceFile(oldFile);
 
-      this.#replaceElement = element;
-      this.#unregisterFile(element);
+    const cropCancelledEvent = () => {
+      this.#uploadButton.dataset.context = oldContext;
+      this.#registerFile(this.#replaceElement!);
+      this.#replaceElement = undefined;
+    };
 
-      clearPreviousErrors(this.#uploadButton);
+    this.#uploadButton.addEventListener("cancel", cropCancelledEvent, { once: true });
 
-      const changeEventListener = () => {
-        this.#fileInput.removeEventListener("cancel", cancelEventListener);
+    void new Promise<void>((resolve) => {
+      this.#uploadResolve = resolve;
+    }).then(() => {
+      this.#uploadResolve = undefined;
+      this.#uploadButton.dataset.context = oldContext;
+      this.#fileInput.removeEventListener("cancel", cropCancelledEvent);
+    });
+  }
 
-        // Wait until the upload starts,
-        // the request to the server is not synchronized with the end of the `change` event.
-        // Otherwise, we would swap the context too soon.
-        void new Promise<void>((resolve) => {
-          this.#uploadResolve = resolve;
-        }).then(() => {
-          this.#uploadResolve = undefined;
-          this.#uploadButton.dataset.context = oldContext;
-        });
-      };
-      const cancelEventListener = () => {
-        this.#uploadButton.dataset.context = oldContext;
-        this.#registerFile(this.#replaceElement!);
-        this.#replaceElement = undefined;
-        this.#fileInput.removeEventListener("change", changeEventListener);
-      };
+  #startReplaceFile(element: WoltlabCoreFileElement): string {
+    // Add to context an extra attribute that the replace button is clicked.
+    // After the dialog is closed or the file is selected, the context will be reset to his old value.
+    // This is necessary as the serverside validation will otherwise fail.
+    const oldContext = this.#uploadButton.dataset.context!;
+    const context = JSON.parse(oldContext);
+    context.__replace = true;
+    this.#uploadButton.dataset.context = JSON.stringify(context);
 
-      this.#fileInput.addEventListener("cancel", cancelEventListener, { once: true });
-      this.#fileInput.addEventListener("change", changeEventListener, { once: true });
-      this.#fileInput.click();
-    });
+    this.#replaceElement = element;
+    this.#unregisterFile(element);
 
-    return replaceButton;
+    return oldContext;
   }
 
   #unregisterFile(element: WoltlabCoreFileElement): void {
index 39545f10bbda616efb1a6d4decfec1122ba91979..9b00148b846c1b64b1b85903285bcc75947a54b6 100644 (file)
@@ -190,6 +190,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
                         void upload(element, resizedFile);
                     })
                         .catch((e) => {
+                        element.dispatchEvent(new CustomEvent("cancel"));
                         if (e === undefined) {
                             // User closed the dialog.
                             return;
index b27cebfe29f5b77b2b6174b857fe4f6f4831cb30..d50e255dc37f7ed885f5e8fde007d2bfabf1b023 100644 (file)
@@ -19,18 +19,31 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Language", "WoltLabSui
         #fileInput;
         #useBigPreview;
         #singleFileUpload;
+        #simpleReplace;
+        #hideDeleteButton;
         #extraButtons;
         #uploadResolve;
-        constructor(fieldId, singleFileUpload = false, useBigPreview = false, extraButtons = []) {
+        constructor(fieldId, singleFileUpload = false, useBigPreview = false, simpleReplace = false, hideDeleteButton = false, extraButtons = []) {
             this.#fieldId = fieldId;
             this.#useBigPreview = useBigPreview;
             this.#singleFileUpload = singleFileUpload;
+            this.#simpleReplace = simpleReplace;
+            this.#hideDeleteButton = hideDeleteButton;
             this.#extraButtons = extraButtons;
             this.#container = document.getElementById(fieldId + "Container");
             if (this.#container === null) {
                 throw new Error("Unknown field with id '" + fieldId + "'");
             }
             this.#uploadButton = this.#container.querySelector("woltlab-core-file-upload");
+            if (this.#simpleReplace) {
+                this.#uploadButton.addEventListener("shouldUpload", () => {
+                    const file = this.#uploadButton.parentElement.querySelector("woltlab-core-file");
+                    if (!file) {
+                        return;
+                    }
+                    this.#simpleFileReplace(file);
+                });
+            }
             this.#uploadButton.addEventListener("uploadStart", (event) => {
                 if (this.#uploadResolve !== undefined) {
                     this.#uploadResolve();
@@ -50,11 +63,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Language", "WoltLabSui
             const buttons = document.createElement("ul");
             buttons.classList.add("buttonList");
             buttons.classList.add(this.classPrefix + "item__buttons");
-            let listItem = document.createElement("li");
-            listItem.append(this.getDeleteButton(element));
-            buttons.append(listItem);
-            if (this.#singleFileUpload) {
-                listItem = document.createElement("li");
+            if (!this.#hideDeleteButton) {
+                const listItem = document.createElement("li");
+                listItem.append(this.getDeleteButton(element));
+                buttons.append(listItem);
+            }
+            if (this.#singleFileUpload && !this.#simpleReplace) {
+                const listItem = document.createElement("li");
                 listItem.append(this.getReplaceButton(element));
                 buttons.append(listItem);
             }
@@ -82,50 +97,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Language", "WoltLabSui
             });
             container.append(buttons);
         }
-        #markElementUploadHasFailed(container, element, reason) {
-            (0, Helper_1.fileInitializationFailed)(container, element, reason);
-            container.classList.add("innerError");
-        }
-        getDeleteButton(element) {
-            const deleteButton = document.createElement("button");
-            deleteButton.type = "button";
-            deleteButton.classList.add("button", "small");
-            deleteButton.textContent = (0, Language_1.getPhrase)("wcf.global.button.delete");
-            deleteButton.addEventListener("click", async () => {
-                const result = await (0, DeleteFile_1.deleteFile)(element.fileId);
-                if (result.ok) {
-                    this.#unregisterFile(element);
-                }
-                else {
-                    let container = element;
-                    if (!this.#useBigPreview) {
-                        container = container.parentElement;
-                    }
-                    if (result.error.code === "permission_denied") {
-                        (0, Util_1.innerError)(container, (0, Language_1.getPhrase)("wcf.upload.error.delete.permissionDenied"), true);
-                    }
-                    else {
-                        (0, Util_1.innerError)(container, result.error.message ?? (0, Language_1.getPhrase)("wcf.upload.error.delete.unknownError"));
-                    }
-                }
-            });
-            return deleteButton;
-        }
         getReplaceButton(element) {
             const replaceButton = document.createElement("button");
             replaceButton.type = "button";
             replaceButton.classList.add("button", "small");
             replaceButton.textContent = (0, Language_1.getPhrase)("wcf.global.button.replace");
             replaceButton.addEventListener("click", () => {
-                // Add to context an extra attribute that the replace button is clicked.
-                // After the dialog is closed or the file is selected, the context will be reset to his old value.
-                // This is necessary as the serverside validation will otherwise fail.
-                const oldContext = this.#uploadButton.dataset.context;
-                const context = JSON.parse(oldContext);
-                context.__replace = true;
-                this.#uploadButton.dataset.context = JSON.stringify(context);
-                this.#replaceElement = element;
-                this.#unregisterFile(element);
+                const oldContext = this.#startReplaceFile(element);
                 (0, Upload_1.clearPreviousErrors)(this.#uploadButton);
                 const changeEventListener = () => {
                     this.#fileInput.removeEventListener("cancel", cancelEventListener);
@@ -151,6 +129,63 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Language", "WoltLabSui
             });
             return replaceButton;
         }
+        #markElementUploadHasFailed(container, element, reason) {
+            (0, Helper_1.fileInitializationFailed)(container, element, reason);
+            container.classList.add("innerError");
+        }
+        getDeleteButton(element) {
+            const deleteButton = document.createElement("button");
+            deleteButton.type = "button";
+            deleteButton.classList.add("button", "small");
+            deleteButton.textContent = (0, Language_1.getPhrase)("wcf.global.button.delete");
+            deleteButton.addEventListener("click", async () => {
+                const result = await (0, DeleteFile_1.deleteFile)(element.fileId);
+                if (result.ok) {
+                    this.#unregisterFile(element);
+                }
+                else {
+                    let container = element;
+                    if (!this.#useBigPreview) {
+                        container = container.parentElement;
+                    }
+                    if (result.error.code === "permission_denied") {
+                        (0, Util_1.innerError)(container, (0, Language_1.getPhrase)("wcf.upload.error.delete.permissionDenied"), true);
+                    }
+                    else {
+                        (0, Util_1.innerError)(container, result.error.message ?? (0, Language_1.getPhrase)("wcf.upload.error.delete.unknownError"));
+                    }
+                }
+            });
+            return deleteButton;
+        }
+        #simpleFileReplace(oldFile) {
+            const oldContext = this.#startReplaceFile(oldFile);
+            const cropCancelledEvent = () => {
+                this.#uploadButton.dataset.context = oldContext;
+                this.#registerFile(this.#replaceElement);
+                this.#replaceElement = undefined;
+            };
+            this.#uploadButton.addEventListener("cancel", cropCancelledEvent, { once: true });
+            void new Promise((resolve) => {
+                this.#uploadResolve = resolve;
+            }).then(() => {
+                this.#uploadResolve = undefined;
+                this.#uploadButton.dataset.context = oldContext;
+                this.#fileInput.removeEventListener("cancel", cropCancelledEvent);
+            });
+        }
+        #startReplaceFile(element) {
+            // Add to context an extra attribute that the replace button is clicked.
+            // After the dialog is closed or the file is selected, the context will be reset to his old value.
+            // This is necessary as the serverside validation will otherwise fail.
+            const oldContext = this.#uploadButton.dataset.context;
+            const context = JSON.parse(oldContext);
+            context.__replace = true;
+            this.#uploadButton.dataset.context = JSON.stringify(context);
+            this.#replaceElement = element;
+            this.#unregisterFile(element);
+            return oldContext;
+        }
         #unregisterFile(element) {
             if (this.#useBigPreview) {
                 element.parentElement.innerHTML = "";
index 0d1c14f6ad740057b9070cccf9ad6445203d1bc9..0f2e2ff5d3d8498e22ad8b5fe740eeeb79037d35 100644 (file)
@@ -45,6 +45,8 @@ final class FileProcessorFormField extends AbstractFormField
     private array $files = [];
     private bool $singleFileUpload = false;
     private bool $bigPreview = false;
+    private bool $simpleReplace = false;
+    private bool $hideDeleteButton = false;
     private array $actionButtons = [];
 
     #[\Override]
@@ -79,6 +81,7 @@ final class FileProcessorFormField extends AbstractFormField
             ),
             'maxUploads' => $this->getFileProcessor()->getMaximumCount($this->context),
             'actionButtons' => $this->actionButtons,
+            'simpleReplace' => $this->simpleReplace,
         ];
     }
 
@@ -136,6 +139,12 @@ final class FileProcessorFormField extends AbstractFormField
             );
         }
 
+        if (!$singleFileUpload && $this->simpleReplace) {
+            throw new \InvalidArgumentException(
+                "Single file upload can't be disabled if the simple replace is enabled for the field '{$this->getId()}'."
+            );
+        }
+
         $this->singleFileUpload = $singleFileUpload;
 
         return $this;
@@ -302,4 +311,48 @@ final class FileProcessorFormField extends AbstractFormField
 
         return $this;
     }
+
+    /**
+     * Returns whether the simple replace is enabled.
+     */
+    public function isSimpleReplace(): bool
+    {
+        return $this->simpleReplace;
+    }
+
+    /**
+     * Sets whether the simple replace is enabled.
+     * Simple replace can only be enabled if single file upload is true.
+     * If enabled, there is no replace button and the existing file is replaced when a new file is uploaded.
+     */
+    public function simpleReplace(bool $simpleReplace = true): self
+    {
+        if ($simpleReplace && !$this->singleFileUpload) {
+            throw new \InvalidArgumentException(
+                "Simple replace can only be enabled for single file uploads for the field '{$this->getId()}'."
+            );
+        }
+
+        $this->simpleReplace = $simpleReplace;
+
+        return $this;
+    }
+
+    /**
+     * Sets whether the delete button should be hidden.
+     */
+    public function hideDeleteButton(bool $hideDeleteButton = true): self
+    {
+        $this->hideDeleteButton = $hideDeleteButton;
+
+        return $this;
+    }
+
+    /**
+     * Returns whether the delete button is hidden.
+     */
+    public function isHideDeleteButton(): bool
+    {
+        return $this->hideDeleteButton;
+    }
 }