Add the prototype for the new popover API
authorAlexander Ebert <ebert@woltlab.com>
Wed, 24 Jan 2024 16:30:53 +0000 (17:30 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 25 Jan 2024 15:29:59 +0000 (16:29 +0100)
com.woltlab.wcf/templates/headIncludeJavaScript.tpl
ts/WoltLabSuite/Core/BootstrapFrontend.ts
ts/WoltLabSuite/Core/Component/Popover.ts [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Popover.js [new file with mode: 0644]
wcfsetup/install/files/lib/action/UserPopoverAction.class.php [new file with mode: 0644]

index 0a8f3a08e20eac1ddd2c5b34967742be9b558876..8dcde35644498883740533942694a6db50f2c42a 100644 (file)
@@ -76,7 +76,7 @@ window.addEventListener('pageshow', function(event) {
                                force: {if $forceBackgroundQueuePerform|isset}true{else}false{/if}
                        },
                        dynamicColorScheme: {if $__wcf->getStyleHandler()->getColorScheme() === 'system'}true{else}false{/if},
-                       enableUserPopover: {if $__wcf->getSession()->getPermission('user.profile.canViewUserProfile')}true{else}false{/if},
+                       endpointUserPopover: {if $__wcf->getSession()->getPermission('user.profile.canViewUserProfile')}'{link controller='UserPopover'}{/link}'{else}''{/if},
                        executeCronjobs: {if $executeCronjobs}'{link controller="CronjobPerform"}{/link}'{else}undefined{/if},
                        {if ENABLE_SHARE_BUTTONS}
                                {assign var='__shareProviders' value="\n"|explode:SHARE_BUTTONS_PROVIDERS}
index f1709ee6d99a3d58f0b121906190690a8aed6388..3f71b88a0f0c69f6ce6c6882fe27bf59697e7729 100644 (file)
@@ -8,7 +8,6 @@
 
 import * as BackgroundQueue from "./BackgroundQueue";
 import * as Bootstrap from "./Bootstrap";
-import * as ControllerPopover from "./Controller/Popover";
 import * as UiUserIgnore from "./Ui/User/Ignore";
 import * as UiPageHeaderMenu from "./Ui/Page/Header/Menu";
 import * as UiMessageUserConsent from "./Ui/Message/UserConsent";
@@ -26,7 +25,7 @@ interface BootstrapOptions {
     force: boolean;
   };
   dynamicColorScheme: boolean;
-  enableUserPopover: boolean;
+  endpointUserPopover: string;
   executeCronjobs: string | undefined;
   shareButtonProviders?: ShareProvider[];
   styleChanger: boolean;
@@ -35,19 +34,19 @@ interface BootstrapOptions {
 /**
  * Initializes user profile popover.
  */
-function _initUserPopover(): void {
-  ControllerPopover.init({
-    className: "userLink",
-    dboAction: "wcf\\data\\user\\UserProfileAction",
-    identifier: "com.woltlab.wcf.user",
-  });
+function setupUserPopover(endpoint: string): void {
+  if (endpoint === "") {
+    return;
+  }
 
-  // @deprecated since 5.3
-  ControllerPopover.init({
-    attributeName: "data-user-id",
-    className: "userLink",
-    dboAction: "wcf\\data\\user\\UserProfileAction",
-    identifier: "com.woltlab.wcf.user.deprecated",
+  whenFirstSeen(".userLink", () => {
+    void import("./Component/Popover").then(({ setupFor }) => {
+      setupFor({
+        endpoint,
+        identifier: "com.woltlab.wcf.user",
+        selector: ".userLink",
+      });
+    });
   });
 }
 
@@ -73,9 +72,7 @@ export function setup(options: BootstrapOptions): void {
     });
   }
 
-  if (options.enableUserPopover) {
-    _initUserPopover();
-  }
+  setupUserPopover(options.endpointUserPopover);
 
   if (options.executeCronjobs !== undefined) {
     void prepareRequest(options.executeCronjobs)
diff --git a/ts/WoltLabSuite/Core/Component/Popover.ts b/ts/WoltLabSuite/Core/Component/Popover.ts
new file mode 100644 (file)
index 0000000..e3af097
--- /dev/null
@@ -0,0 +1,95 @@
+import { prepareRequest } from "../Ajax/Backend";
+import DomUtil from "../Dom/Util";
+import { getPageOverlayContainer } from "../Helper/PageOverlay";
+import { wheneverFirstSeen } from "../Helper/Selector";
+
+class Popover {
+  readonly #cache = new Map<number, string>();
+  #currentElement: HTMLElement | undefined = undefined;
+  #container: HTMLElement | undefined = undefined;
+  readonly #endpoint: URL;
+  #enabled = true;
+  readonly #identifier: string;
+
+  constructor(selector: string, endpoint: string, identifier: string) {
+    this.#identifier = identifier;
+    this.#endpoint = new URL(endpoint);
+
+    wheneverFirstSeen(selector, (element) => {
+      element.addEventListener("mouseenter", () => {
+        void this.#hoverStart(element);
+      });
+      element.addEventListener("mouseleave", () => {
+        this.#hoverEnd(element);
+      });
+    });
+
+    const mq = window.matchMedia("(hover:hover)");
+    this.#setEnabled(mq.matches);
+
+    mq.addEventListener("change", (event) => {
+      this.#setEnabled(event.matches);
+    });
+
+    window.addEventListener("beforeunload", () => {
+      this.#setEnabled(false);
+    });
+  }
+
+  async #hoverStart(element: HTMLElement): Promise<void> {
+    const objectId = this.#getObjectId(element);
+
+    let content = this.#cache.get(objectId);
+    if (content === undefined) {
+      content = await this.#fetch(objectId);
+      this.#cache.set(objectId, content);
+    }
+
+    DomUtil.setInnerHtml(this.#getContainer(), content);
+  }
+
+  #hoverEnd(element: HTMLElement): void {}
+
+  async #fetch(objectId: number): Promise<string> {
+    this.#endpoint.searchParams.set("id", objectId.toString());
+
+    const response = await prepareRequest(this.#endpoint).get().fetchAsResponse();
+    if (response?.ok) {
+      return await response.text();
+    }
+
+    return "";
+  }
+
+  #setEnabled(enabled: boolean): void {
+    this.#enabled = enabled;
+  }
+
+  #getObjectId(element: HTMLElement): number {
+    return parseInt(element.dataset.objectId!);
+  }
+
+  #getContainer(): HTMLElement {
+    if (this.#container === undefined) {
+      this.#container = document.createElement("div");
+      this.#container.dataset.identifier = this.#identifier;
+    }
+
+    this.#container.remove();
+    getPageOverlayContainer().append(this.#container);
+
+    return this.#container;
+  }
+}
+
+type Configuration = {
+  endpoint: string;
+  identifier: string;
+  selector: string;
+};
+
+export function setupFor(configuration: Configuration): void {
+  const { identifier, endpoint, selector } = configuration;
+
+  new Popover(selector, endpoint, identifier);
+}
index 25abf088832a33ab38baf1dd8484788929ea2265..bc221165ca4240a11ec8606e585bd8a5aaa99c67 100644 (file)
@@ -5,13 +5,12 @@
  * @copyright  2001-2019 WoltLab GmbH
  * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  */
-define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Controller/Popover", "./Ui/User/Ignore", "./Ui/Page/Header/Menu", "./Ui/Message/UserConsent", "./Ui/Message/Share/Dialog", "./Ui/Message/Share/Providers", "./Ui/Feed/Dialog", "./User", "./Ui/Page/Menu/Main/Frontend", "./LazyLoader", "./Ajax/Backend"], function (require, exports, tslib_1, BackgroundQueue, Bootstrap, ControllerPopover, UiUserIgnore, UiPageHeaderMenu, UiMessageUserConsent, UiMessageShareDialog, Providers_1, UiFeedDialog, User_1, Frontend_1, LazyLoader_1, Backend_1) {
+define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui/User/Ignore", "./Ui/Page/Header/Menu", "./Ui/Message/UserConsent", "./Ui/Message/Share/Dialog", "./Ui/Message/Share/Providers", "./Ui/Feed/Dialog", "./User", "./Ui/Page/Menu/Main/Frontend", "./LazyLoader", "./Ajax/Backend"], function (require, exports, tslib_1, BackgroundQueue, Bootstrap, UiUserIgnore, UiPageHeaderMenu, UiMessageUserConsent, UiMessageShareDialog, Providers_1, UiFeedDialog, User_1, Frontend_1, LazyLoader_1, Backend_1) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.setup = void 0;
     BackgroundQueue = tslib_1.__importStar(BackgroundQueue);
     Bootstrap = tslib_1.__importStar(Bootstrap);
-    ControllerPopover = tslib_1.__importStar(ControllerPopover);
     UiUserIgnore = tslib_1.__importStar(UiUserIgnore);
     UiPageHeaderMenu = tslib_1.__importStar(UiPageHeaderMenu);
     UiMessageUserConsent = tslib_1.__importStar(UiMessageUserConsent);
@@ -22,18 +21,18 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Co
     /**
      * Initializes user profile popover.
      */
-    function _initUserPopover() {
-        ControllerPopover.init({
-            className: "userLink",
-            dboAction: "wcf\\data\\user\\UserProfileAction",
-            identifier: "com.woltlab.wcf.user",
-        });
-        // @deprecated since 5.3
-        ControllerPopover.init({
-            attributeName: "data-user-id",
-            className: "userLink",
-            dboAction: "wcf\\data\\user\\UserProfileAction",
-            identifier: "com.woltlab.wcf.user.deprecated",
+    function setupUserPopover(endpoint) {
+        if (endpoint === "") {
+            return;
+        }
+        (0, LazyLoader_1.whenFirstSeen)(".userLink", () => {
+            void new Promise((resolve_1, reject_1) => { require(["./Component/Popover"], resolve_1, reject_1); }).then(tslib_1.__importStar).then(({ setupFor }) => {
+                setupFor({
+                    endpoint,
+                    identifier: "com.woltlab.wcf.user",
+                    selector: ".userLink",
+                });
+            });
         });
     }
     /**
@@ -49,13 +48,11 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Co
         });
         UiPageHeaderMenu.init();
         if (options.styleChanger) {
-            void new Promise((resolve_1, reject_1) => { require(["./Controller/Style/Changer"], resolve_1, reject_1); }).then(tslib_1.__importStar).then((ControllerStyleChanger) => {
+            void new Promise((resolve_2, reject_2) => { require(["./Controller/Style/Changer"], resolve_2, reject_2); }).then(tslib_1.__importStar).then((ControllerStyleChanger) => {
                 ControllerStyleChanger.setup();
             });
         }
-        if (options.enableUserPopover) {
-            _initUserPopover();
-        }
+        setupUserPopover(options.endpointUserPopover);
         if (options.executeCronjobs !== undefined) {
             void (0, Backend_1.prepareRequest)(options.executeCronjobs)
                 .get()
@@ -82,16 +79,16 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Co
             UiFeedDialog.setup();
         }
         (0, LazyLoader_1.whenFirstSeen)("woltlab-core-reaction-summary", () => {
-            void new Promise((resolve_2, reject_2) => { require(["./Ui/Reaction/SummaryDetails"], resolve_2, reject_2); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+            void new Promise((resolve_3, reject_3) => { require(["./Ui/Reaction/SummaryDetails"], resolve_3, reject_3); }).then(tslib_1.__importStar).then(({ setup }) => setup());
         });
         (0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment", () => {
-            void new Promise((resolve_3, reject_3) => { require(["./Component/Comment/woltlab-core-comment"], resolve_3, reject_3); }).then(tslib_1.__importStar);
+            void new Promise((resolve_4, reject_4) => { require(["./Component/Comment/woltlab-core-comment"], resolve_4, reject_4); }).then(tslib_1.__importStar);
         });
         (0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment-response", () => {
-            void new Promise((resolve_4, reject_4) => { require(["./Component/Comment/Response/woltlab-core-comment-response"], resolve_4, reject_4); }).then(tslib_1.__importStar);
+            void new Promise((resolve_5, reject_5) => { require(["./Component/Comment/Response/woltlab-core-comment-response"], resolve_5, reject_5); }).then(tslib_1.__importStar);
         });
         (0, LazyLoader_1.whenFirstSeen)("[data-follow-user]", () => {
-            void new Promise((resolve_5, reject_5) => { require(["./Component/User/Follow"], resolve_5, reject_5); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+            void new Promise((resolve_6, reject_6) => { require(["./Component/User/Follow"], resolve_6, reject_6); }).then(tslib_1.__importStar).then(({ setup }) => setup());
         });
         (0, LazyLoader_1.whenFirstSeen)("[data-ignore-user]", () => {
             void new Promise((resolve_6, reject_6) => { require(["./Component/User/Ignore"], resolve_6, reject_6); }).then(tslib_1.__importStar).then(({ setup }) => setup());
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Popover.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Popover.js
new file mode 100644 (file)
index 0000000..e090260
--- /dev/null
@@ -0,0 +1,72 @@
+define(["require", "exports", "tslib", "../Ajax/Backend", "../Dom/Util", "../Helper/PageOverlay", "../Helper/Selector"], function (require, exports, tslib_1, Backend_1, Util_1, PageOverlay_1, Selector_1) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.setupFor = void 0;
+    Util_1 = tslib_1.__importDefault(Util_1);
+    class Popover {
+        #cache = new Map();
+        #currentElement = undefined;
+        #container = undefined;
+        #endpoint;
+        #enabled = true;
+        #identifier;
+        constructor(selector, endpoint, identifier) {
+            this.#identifier = identifier;
+            this.#endpoint = new URL(endpoint);
+            (0, Selector_1.wheneverFirstSeen)(selector, (element) => {
+                element.addEventListener("mouseenter", () => {
+                    void this.#hoverStart(element);
+                });
+                element.addEventListener("mouseleave", () => {
+                    this.#hoverEnd(element);
+                });
+            });
+            const mq = window.matchMedia("(hover:hover)");
+            this.#setEnabled(mq.matches);
+            mq.addEventListener("change", (event) => {
+                this.#setEnabled(event.matches);
+            });
+            window.addEventListener("beforeunload", () => {
+                this.#setEnabled(false);
+            });
+        }
+        async #hoverStart(element) {
+            const objectId = this.#getObjectId(element);
+            let content = this.#cache.get(objectId);
+            if (content === undefined) {
+                content = await this.#fetch(objectId);
+                this.#cache.set(objectId, content);
+            }
+            Util_1.default.setInnerHtml(this.#getContainer(), content);
+        }
+        #hoverEnd(element) { }
+        async #fetch(objectId) {
+            this.#endpoint.searchParams.set("id", objectId.toString());
+            const response = await (0, Backend_1.prepareRequest)(this.#endpoint).get().fetchAsResponse();
+            if (response?.ok) {
+                return await response.text();
+            }
+            return "";
+        }
+        #setEnabled(enabled) {
+            this.#enabled = enabled;
+        }
+        #getObjectId(element) {
+            return parseInt(element.dataset.objectId);
+        }
+        #getContainer() {
+            if (this.#container === undefined) {
+                this.#container = document.createElement("div");
+                this.#container.dataset.identifier = this.#identifier;
+            }
+            this.#container.remove();
+            (0, PageOverlay_1.getPageOverlayContainer)().append(this.#container);
+            return this.#container;
+        }
+    }
+    function setupFor(configuration) {
+        const { identifier, endpoint, selector } = configuration;
+        new Popover(selector, endpoint, identifier);
+    }
+    exports.setupFor = setupFor;
+});
diff --git a/wcfsetup/install/files/lib/action/UserPopoverAction.class.php b/wcfsetup/install/files/lib/action/UserPopoverAction.class.php
new file mode 100644 (file)
index 0000000..8e5a2cc
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+namespace wcf\action;
+
+use Laminas\Diactoros\Response\HtmlResponse;
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use wcf\data\user\group\UserGroup;
+use wcf\data\user\UserProfile;
+use wcf\data\user\UserProfileList;
+use wcf\http\Helper;
+use wcf\system\cache\runtime\UserProfileRuntimeCache;
+use wcf\system\WCF;
+
+/**
+ * Provides the popover content for a user.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+final class UserPopoverAction implements RequestHandlerInterface
+{
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
+        $parameters = Helper::mapQueryParameters(
+            $request->getQueryParams(),
+            <<<'EOT'
+                    array {
+                        id: positive-int
+                    }
+                    EOT,
+        );
+
+        $userProfile = UserProfileRuntimeCache::getInstance()->getObject($parameters['id']);
+        if ($userProfile) {
+            WCF::getTPL()->assign('user', $userProfile);
+        } else {
+            WCF::getTPL()->assign('unknownUser', true);
+        }
+
+        return new HtmlResponse(
+            WCF::getTPL()->fetch('userProfilePreview'),
+        );
+    }
+}