Manage each popover separately to avoid shared state
authorAlexander Ebert <ebert@woltlab.com>
Thu, 25 Jan 2024 11:06:49 +0000 (12:06 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 25 Jan 2024 15:29:59 +0000 (16:29 +0100)
ts/WoltLabSuite/Core/Component/Popover.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Popover.js

index 85436257050bffd612acc0f542a6d9b5ce5c208a..10b5d0e147879b388e4a4bbb719490b7cc7e7993 100644 (file)
@@ -14,22 +14,21 @@ class Popover {
   readonly #cache: SharedCache;
   #container: HTMLElement | undefined = undefined;
   #enabled = true;
+  readonly #element: HTMLElement;
   readonly #identifier: string;
-  #pendingObjectId: number | undefined = undefined;
-  #timerStart: RepeatingTimer | undefined = undefined;
+  #timerShouldShow: RepeatingTimer | undefined = undefined;
   #timerHide: RepeatingTimer | undefined = undefined;
 
-  constructor(cache: SharedCache, selector: string, identifier: string) {
+  constructor(cache: SharedCache, element: HTMLElement, identifier: string) {
     this.#cache = cache;
+    this.#element = element;
     this.#identifier = identifier;
 
-    wheneverFirstSeen(selector, (element) => {
-      element.addEventListener("mouseenter", () => {
-        this.#showPopover(element);
-      });
-      element.addEventListener("mouseleave", () => {
-        this.#hidePopover();
-      });
+    element.addEventListener("mouseenter", () => {
+      this.#showPopover();
+    });
+    element.addEventListener("mouseleave", () => {
+      this.#hidePopover();
     });
 
     const mq = window.matchMedia("(hover:hover)");
@@ -42,41 +41,39 @@ class Popover {
     window.addEventListener("beforeunload", () => {
       this.#setEnabled(false);
     });
+
+    this.#showPopover();
   }
 
-  #showPopover(element: HTMLElement): void {
-    const objectId = this.#getObjectId(element);
+  #showPopover(): void {
+    this.#timerHide?.stop();
 
-    this.#pendingObjectId = objectId;
-    if (this.#timerStart === undefined) {
-      this.#timerStart = new RepeatingTimer((timer) => {
+    if (this.#timerShouldShow === undefined) {
+      this.#timerShouldShow = new RepeatingTimer((timer) => {
         timer.stop();
 
-        const objectId = this.#pendingObjectId!;
+        const objectId = this.#getObjectId();
         void this.#cache.get(objectId).then((content) => {
-          if (objectId !== this.#pendingObjectId) {
-            return;
-          }
-
           const container = this.#getContainer();
           DomUtil.setInnerHtml(container, content);
 
-          UiAlignment.set(container, element, { vertical: "top" });
+          UiAlignment.set(container, this.#element, { vertical: "top" });
 
           container.setAttribute("aria-hidden", "false");
         });
       }, Delay.Show);
     } else {
-      this.#timerStart.restart();
+      this.#timerShouldShow.restart();
     }
   }
 
   #hidePopover(): void {
+    this.#timerShouldShow?.stop();
+
     if (this.#timerHide === undefined) {
       this.#timerHide = new RepeatingTimer((timer) => {
         timer.stop();
 
-        this.#timerStart?.stop();
         this.#container?.setAttribute("aria-hidden", "true");
       }, Delay.Hide);
     } else {
@@ -88,8 +85,8 @@ class Popover {
     this.#enabled = enabled;
   }
 
-  #getObjectId(element: HTMLElement): number {
-    return parseInt(element.dataset.objectId!);
+  #getObjectId(): number {
+    return parseInt(this.#element.dataset.objectId!);
   }
 
   #getContainer(): HTMLElement {
@@ -98,10 +95,17 @@ class Popover {
       this.#container.classList.add("popoverContainer");
       this.#container.dataset.identifier = this.#identifier;
       this.#container.setAttribute("aria-hidden", "true");
+
+      this.#container.addEventListener("transitionend", () => {
+        if (this.#container!.getAttribute("aria-hidden") === "true") {
+          this.#container!.remove();
+        }
+      });
     }
 
-    this.#container.remove();
-    getPageOverlayContainer().append(this.#container);
+    if (this.#container.parentNode === null) {
+      getPageOverlayContainer().append(this.#container);
+    }
 
     return this.#container;
   }
@@ -118,5 +122,13 @@ export function setupFor(configuration: Configuration): void {
 
   const cache = new SharedCache(endpoint);
 
-  new Popover(cache, selector, identifier);
+  wheneverFirstSeen(selector, (element) => {
+    element.addEventListener(
+      "mouseenter",
+      () => {
+        new Popover(cache, element, identifier);
+      },
+      { once: true },
+    );
+  });
 }
index 5257c08b1553ee16dcad823e69f0799d5bbbb991..1753fff5a83d7696db73ccdf441f30459f704224 100644 (file)
@@ -10,20 +10,19 @@ define(["require", "exports", "tslib", "../Dom/Util", "../Helper/PageOverlay", "
         #cache;
         #container = undefined;
         #enabled = true;
+        #element;
         #identifier;
-        #pendingObjectId = undefined;
-        #timerStart = undefined;
+        #timerShouldShow = undefined;
         #timerHide = undefined;
-        constructor(cache, selector, identifier) {
+        constructor(cache, element, identifier) {
             this.#cache = cache;
+            this.#element = element;
             this.#identifier = identifier;
-            (0, Selector_1.wheneverFirstSeen)(selector, (element) => {
-                element.addEventListener("mouseenter", () => {
-                    this.#showPopover(element);
-                });
-                element.addEventListener("mouseleave", () => {
-                    this.#hidePopover();
-                });
+            element.addEventListener("mouseenter", () => {
+                this.#showPopover();
+            });
+            element.addEventListener("mouseleave", () => {
+                this.#hidePopover();
             });
             const mq = window.matchMedia("(hover:hover)");
             this.#setEnabled(mq.matches);
@@ -33,34 +32,31 @@ define(["require", "exports", "tslib", "../Dom/Util", "../Helper/PageOverlay", "
             window.addEventListener("beforeunload", () => {
                 this.#setEnabled(false);
             });
+            this.#showPopover();
         }
-        #showPopover(element) {
-            const objectId = this.#getObjectId(element);
-            this.#pendingObjectId = objectId;
-            if (this.#timerStart === undefined) {
-                this.#timerStart = new Repeating_1.default((timer) => {
+        #showPopover() {
+            this.#timerHide?.stop();
+            if (this.#timerShouldShow === undefined) {
+                this.#timerShouldShow = new Repeating_1.default((timer) => {
                     timer.stop();
-                    const objectId = this.#pendingObjectId;
+                    const objectId = this.#getObjectId();
                     void this.#cache.get(objectId).then((content) => {
-                        if (objectId !== this.#pendingObjectId) {
-                            return;
-                        }
                         const container = this.#getContainer();
                         Util_1.default.setInnerHtml(container, content);
-                        UiAlignment.set(container, element, { vertical: "top" });
+                        UiAlignment.set(container, this.#element, { vertical: "top" });
                         container.setAttribute("aria-hidden", "false");
                     });
                 }, 800 /* Delay.Show */);
             }
             else {
-                this.#timerStart.restart();
+                this.#timerShouldShow.restart();
             }
         }
         #hidePopover() {
+            this.#timerShouldShow?.stop();
             if (this.#timerHide === undefined) {
                 this.#timerHide = new Repeating_1.default((timer) => {
                     timer.stop();
-                    this.#timerStart?.stop();
                     this.#container?.setAttribute("aria-hidden", "true");
                 }, 500 /* Delay.Hide */);
             }
@@ -71,8 +67,8 @@ define(["require", "exports", "tslib", "../Dom/Util", "../Helper/PageOverlay", "
         #setEnabled(enabled) {
             this.#enabled = enabled;
         }
-        #getObjectId(element) {
-            return parseInt(element.dataset.objectId);
+        #getObjectId() {
+            return parseInt(this.#element.dataset.objectId);
         }
         #getContainer() {
             if (this.#container === undefined) {
@@ -80,16 +76,26 @@ define(["require", "exports", "tslib", "../Dom/Util", "../Helper/PageOverlay", "
                 this.#container.classList.add("popoverContainer");
                 this.#container.dataset.identifier = this.#identifier;
                 this.#container.setAttribute("aria-hidden", "true");
+                this.#container.addEventListener("transitionend", () => {
+                    if (this.#container.getAttribute("aria-hidden") === "true") {
+                        this.#container.remove();
+                    }
+                });
+            }
+            if (this.#container.parentNode === null) {
+                (0, PageOverlay_1.getPageOverlayContainer)().append(this.#container);
             }
-            this.#container.remove();
-            (0, PageOverlay_1.getPageOverlayContainer)().append(this.#container);
             return this.#container;
         }
     }
     function setupFor(configuration) {
         const { identifier, endpoint, selector } = configuration;
         const cache = new SharedCache_1.default(endpoint);
-        new Popover(cache, selector, identifier);
+        (0, Selector_1.wheneverFirstSeen)(selector, (element) => {
+            element.addEventListener("mouseenter", () => {
+                new Popover(cache, element, identifier);
+            }, { once: true });
+        });
     }
     exports.setupFor = setupFor;
 });