Add support for async callbacks in `validate`
authorAlexander Ebert <ebert@woltlab.com>
Fri, 18 Nov 2022 13:52:37 +0000 (14:52 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Fri, 18 Nov 2022 13:52:37 +0000 (14:52 +0100)
ts/WoltLabSuite/Core/Element/woltlab-core-dialog.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-dialog.js

index c313523db6bff0275faf4a2fdb4fb56e21e26b1a..1e439355d8d88f6923375724e8251283462adbf1 100644 (file)
@@ -15,6 +15,8 @@ import { adoptPageOverlayContainer, releasePageOverlayContainer } from "../Helpe
 import * as Language from "../Language";
 import { scrollDisable, scrollEnable } from "../Ui/Screen";
 
+type ValidateCallback = Promise<boolean>;
+
 interface WoltlabCoreDialogEventMap {
   afterClose: CustomEvent;
   backdrop: CustomEvent;
@@ -22,7 +24,7 @@ interface WoltlabCoreDialogEventMap {
   close: CustomEvent;
   extra: CustomEvent;
   primary: CustomEvent;
-  validate: CustomEvent;
+  validate: CustomEvent<ValidateCallback[]>;
 }
 
 const dialogContainer = document.createElement("div");
@@ -144,12 +146,40 @@ export class WoltlabCoreDialogElement extends HTMLElement {
         return;
       }
 
-      const evt = new CustomEvent("validate", { cancelable: true });
+      const callbacks: ValidateCallback[] = [];
+      const evt = new CustomEvent("validate", {
+        cancelable: true,
+        detail: callbacks,
+      });
       this.dispatchEvent(evt);
 
       if (evt.defaultPrevented) {
         event.preventDefault();
       }
+
+      if (evt.detail.length > 0) {
+        // DOM events cannot wait for async functions. We must
+        // reject the event and then wait for the async
+        // callbacks to complete.
+        event.preventDefault();
+
+        // Blocking further attempts to submit the dialog
+        // while the validation is running.
+        this.incomplete = true;
+
+        void Promise.all(evt.detail).then((results) => {
+          this.incomplete = false;
+
+          const failedValidation = results.some((result) => result === false);
+          if (!failedValidation) {
+            // The `primary` event is triggered once the validation
+            // has completed. Triggering the submit again would cause
+            // `validate` to run again, causing an infinite loop.
+            this.#dispatchPrimaryEvent();
+            this.#dialog.close();
+          }
+        });
+      }
     });
 
     this.#dialog.addEventListener("close", () => {
@@ -158,8 +188,7 @@ export class WoltlabCoreDialogElement extends HTMLElement {
         return;
       }
 
-      const evt = new CustomEvent("primary");
-      this.dispatchEvent(evt);
+      this.#dispatchPrimaryEvent();
     });
 
     formControl.addEventListener("cancel", () => {
@@ -179,6 +208,11 @@ export class WoltlabCoreDialogElement extends HTMLElement {
     }
   }
 
+  #dispatchPrimaryEvent(): void {
+    const evt = new CustomEvent("primary");
+    this.dispatchEvent(evt);
+  }
+
   connectedCallback(): void {
     if (this.#dialog.parentElement !== null) {
       return;
index 374b5f547c63b6a6b58bc4f086a393b3d634dc81..e8a54ce83e6b8e7c0012b7720a3e1ec522d3cebe 100644 (file)
@@ -102,19 +102,42 @@ define(["require", "exports", "tslib", "../Dom/Util", "../Helper/PageOverlay", "
                     event.preventDefault();
                     return;
                 }
-                const evt = new CustomEvent("validate", { cancelable: true });
+                const callbacks = [];
+                const evt = new CustomEvent("validate", {
+                    cancelable: true,
+                    detail: callbacks,
+                });
                 this.dispatchEvent(evt);
                 if (evt.defaultPrevented) {
                     event.preventDefault();
                 }
+                if (evt.detail.length > 0) {
+                    // DOM events cannot wait for async functions. We must
+                    // reject the event and then wait for the async
+                    // callbacks to complete.
+                    event.preventDefault();
+                    // Blocking further attempts to submit the dialog
+                    // while the validation is running.
+                    this.incomplete = true;
+                    void Promise.all(evt.detail).then((results) => {
+                        this.incomplete = false;
+                        const failedValidation = results.some((result) => result === false);
+                        if (!failedValidation) {
+                            // The `primary` event is triggered once the validation
+                            // has completed. Triggering the submit again would cause
+                            // `validate` to run again, causing an infinite loop.
+                            this.#dispatchPrimaryEvent();
+                            this.#dialog.close();
+                        }
+                    });
+                }
             });
             this.#dialog.addEventListener("close", () => {
                 if (this.#dialog.returnValue === "") {
                     // Dialog was programmatically closed.
                     return;
                 }
-                const evt = new CustomEvent("primary");
-                this.dispatchEvent(evt);
+                this.#dispatchPrimaryEvent();
             });
             formControl.addEventListener("cancel", () => {
                 const event = new CustomEvent("cancel", { cancelable: true });
@@ -130,6 +153,10 @@ define(["require", "exports", "tslib", "../Dom/Util", "../Helper/PageOverlay", "
                 });
             }
         }
+        #dispatchPrimaryEvent() {
+            const evt = new CustomEvent("primary");
+            this.dispatchEvent(evt);
+        }
         connectedCallback() {
             if (this.#dialog.parentElement !== null) {
                 return;