Add `[data-formbuilder]` and `Psr15DialogFormResponse`
authorAlexander Ebert <ebert@woltlab.com>
Sat, 12 Aug 2023 17:29:33 +0000 (19:29 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 12 Aug 2023 17:29:33 +0000 (19:29 +0200)
ts/WoltLabSuite/Core/Bootstrap.ts
ts/WoltLabSuite/Core/Component/FormBuilder/Button.ts [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/FormBuilder/Button.js [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/Psr15DialogFormResponse.class.php [new file with mode: 0644]

index c290f507d792666c37a434082c94d2058eeb0472..82bb085bc6babe4ebb939d1ee1249cef1df636c3 100644 (file)
@@ -32,6 +32,7 @@ import { init as initSearch } from "./Ui/Search";
 import { PageMenuMainProvider } from "./Ui/Page/Menu/Main/Provider";
 import { whenFirstSeen } from "./LazyLoader";
 import { adoptPageOverlayContainer } from "./Helper/PageOverlay";
+import { setup as setupFormBuilderButton } from "./Component/FormBuilder/Button";
 
 // perfectScrollbar does not need to be bound anywhere, it just has to be loaded for WCF.js
 import "perfect-scrollbar";
@@ -103,6 +104,7 @@ export function setup(options: BoostrapOptions): void {
   UiObjectActionDelete.setup();
   UiObjectActionToggle.setup();
   initSearch();
+  setupFormBuilderButton();
 
   // Convert forms with `method="get"` into `method="post"`
   document.querySelectorAll("form[method=get]").forEach((form: HTMLFormElement) => {
diff --git a/ts/WoltLabSuite/Core/Component/FormBuilder/Button.ts b/ts/WoltLabSuite/Core/Component/FormBuilder/Button.ts
new file mode 100644 (file)
index 0000000..e84050e
--- /dev/null
@@ -0,0 +1,81 @@
+/**
+ * Binds to button-like elements with the attribute [data-formbuilder] and invokes
+ * the endpoint to request the form builder dialog.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+
+import { dialogFactory } from "../Dialog";
+import { wheneverSeen } from "../../Helper/Selector";
+
+const reponseIdentifier = "__Psr15DialogFormResponse";
+
+type Psr15DialogFormResponse = {
+  payload:
+    | {
+        reload: true;
+      }
+    | {
+        redirectUrl: string;
+      };
+  __Psr15DialogFormResponse: true;
+};
+
+async function requestForm(element: HTMLElement): Promise<void> {
+  const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint(element.dataset.endpoint!);
+  if (!ok) {
+    return;
+  }
+
+  const event = new CustomEvent<unknown>("formBuilder:result", {
+    cancelable: true,
+    detail: {
+      result,
+    },
+  });
+  element.dispatchEvent(event);
+
+  if (event.defaultPrevented) {
+    return;
+  }
+
+  if (typeof result === "object" && result !== null && Object.hasOwn(result, reponseIdentifier)) {
+    const payload = (result as Psr15DialogFormResponse).payload;
+    if ("reload" in payload) {
+      window.location.reload();
+    } else {
+      window.location.href = payload.redirectUrl;
+    }
+
+    return;
+  }
+}
+
+export function setup(): void {
+  wheneverSeen("[data-formbuilder]", (element) => {
+    if (element.tagName !== "A" && element.tagName !== "BUTTON") {
+      throw new TypeError("Cannot initialize the FormBuilder on non button-like elements", {
+        cause: {
+          element,
+        },
+      });
+    }
+
+    if (!element.dataset.endpoint) {
+      throw new Error("Missing the [data-endpoint] attribute.", {
+        cause: {
+          element,
+        },
+      });
+    }
+
+    element.addEventListener("click", (event) => {
+      event.preventDefault();
+
+      void requestForm(element);
+    });
+  });
+}
index b7cd0cb62814c477235da333915c4301548de3eb..07820c00e90df548432a62abdba669f6d31cefce 100644 (file)
@@ -7,7 +7,7 @@
  * @copyright  2001-2022 WoltLab GmbH
  * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  */
-define(["require", "exports", "tslib", "./Core", "./Date/Picker", "./Devtools", "./Dom/Change/Listener", "./Environment", "./Event/Handler", "./Form/XsrfToken", "./Language", "./Ui/Dialog", "./Ui/Dropdown/Simple", "./Ui/Mobile", "./Ui/Page/Action", "./Ui/TabMenu", "./Ui/Tooltip", "./Ui/Page/JumpTo", "./Ui/Password", "./Ui/Empty", "./Ui/Object/Action", "./Ui/Object/Action/Delete", "./Ui/Object/Action/Toggle", "./Ui/Search", "./LazyLoader", "./Helper/PageOverlay", "perfect-scrollbar"], function (require, exports, tslib_1, Core, Picker_1, Devtools_1, Listener_1, Environment, EventHandler, XsrfToken, Language, Dialog_1, Simple_1, UiMobile, UiPageAction, UiTabMenu, UiTooltip, UiPageJumpTo, UiPassword, UiEmpty, UiObjectAction, UiObjectActionDelete, UiObjectActionToggle, Search_1, LazyLoader_1, PageOverlay_1) {
+define(["require", "exports", "tslib", "./Core", "./Date/Picker", "./Devtools", "./Dom/Change/Listener", "./Environment", "./Event/Handler", "./Form/XsrfToken", "./Language", "./Ui/Dialog", "./Ui/Dropdown/Simple", "./Ui/Mobile", "./Ui/Page/Action", "./Ui/TabMenu", "./Ui/Tooltip", "./Ui/Page/JumpTo", "./Ui/Password", "./Ui/Empty", "./Ui/Object/Action", "./Ui/Object/Action/Delete", "./Ui/Object/Action/Toggle", "./Ui/Search", "./LazyLoader", "./Helper/PageOverlay", "./Component/FormBuilder/Button", "perfect-scrollbar"], function (require, exports, tslib_1, Core, Picker_1, Devtools_1, Listener_1, Environment, EventHandler, XsrfToken, Language, Dialog_1, Simple_1, UiMobile, UiPageAction, UiTabMenu, UiTooltip, UiPageJumpTo, UiPassword, UiEmpty, UiObjectAction, UiObjectActionDelete, UiObjectActionToggle, Search_1, LazyLoader_1, PageOverlay_1, Button_1) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.setup = void 0;
@@ -82,6 +82,7 @@ define(["require", "exports", "tslib", "./Core", "./Date/Picker", "./Devtools",
         UiObjectActionDelete.setup();
         UiObjectActionToggle.setup();
         (0, Search_1.init)();
+        (0, Button_1.setup)();
         // Convert forms with `method="get"` into `method="post"`
         document.querySelectorAll("form[method=get]").forEach((form) => {
             form.method = "post";
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/FormBuilder/Button.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/FormBuilder/Button.js
new file mode 100644 (file)
index 0000000..8541862
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Binds to button-like elements with the attribute [data-formbuilder] and invokes
+ * the endpoint to request the form builder dialog.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+define(["require", "exports", "../Dialog", "../../Helper/Selector"], function (require, exports, Dialog_1, Selector_1) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.setup = void 0;
+    const reponseIdentifier = "__Psr15DialogFormResponse";
+    async function requestForm(element) {
+        const { ok, result } = await (0, Dialog_1.dialogFactory)().usingFormBuilder().fromEndpoint(element.dataset.endpoint);
+        if (!ok) {
+            return;
+        }
+        const event = new CustomEvent("formBuilder:result", {
+            cancelable: true,
+            detail: {
+                result,
+            },
+        });
+        element.dispatchEvent(event);
+        if (event.defaultPrevented) {
+            return;
+        }
+        if (typeof result === "object" && result !== null && Object.hasOwn(result, reponseIdentifier)) {
+            const payload = result.payload;
+            if ("reload" in payload) {
+                window.location.reload();
+            }
+            else {
+                window.location.href = payload.redirectUrl;
+            }
+            return;
+        }
+    }
+    function setup() {
+        (0, Selector_1.wheneverSeen)("[data-formbuilder]", (element) => {
+            if (element.tagName !== "A" && element.tagName !== "BUTTON") {
+                throw new TypeError("Cannot initialize the FormBuilder on non button-like elements", {
+                    cause: {
+                        element,
+                    },
+                });
+            }
+            if (!element.dataset.endpoint) {
+                throw new Error("Missing the [data-endpoint] attribute.", {
+                    cause: {
+                        element,
+                    },
+                });
+            }
+            element.addEventListener("click", (event) => {
+                event.preventDefault();
+                void requestForm(element);
+            });
+        });
+    }
+    exports.setup = setup;
+});
diff --git a/wcfsetup/install/files/lib/system/form/builder/Psr15DialogFormResponse.class.php b/wcfsetup/install/files/lib/system/form/builder/Psr15DialogFormResponse.class.php
new file mode 100644 (file)
index 0000000..8b82b11
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+namespace wcf\system\form\builder;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Creates a response that is understood by the `[data-formbuilder]` implementation
+ * as a shortcut for tasks like reloading the page or redirecting to another URL.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+final class Psr15DialogFormResponse
+{
+    private readonly array $payload;
+
+    private const RESPONSE_IDENTIFIER = "__Psr15DialogFormResponse";
+
+    /**
+     * Redirects the client to the provided URL.
+     */
+    public static function redirect(string $redirectUrl): self
+    {
+        return new self([
+            "redirectUrl" => $redirectUrl,
+        ]);
+    }
+
+    /**
+     * Instructs the client to reload the page.
+     */
+    public static function reload(): self
+    {
+        return new self([
+            "reload" => true,
+        ]);
+    }
+
+    /**
+     * Converts this into a PSR response.
+     */
+    public function toResponse(): ResponseInterface
+    {
+        return new JsonResponse([
+            'payload' => $this->payload,
+            self::RESPONSE_IDENTIFIER => true,
+        ]);
+    }
+
+    private function __construct(array $payload)
+    {
+        $this->payload = $payload;
+    }
+}