Automatically infer the return type of Dom/Traverse#child(ren)?ByTag
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / ts / WoltLabSuite / Core / Ui / TabMenu / Simple.ts
index effbaf7370853446563956416d7b5f4365ac36db..b2eb37eeb48936db22483a1b3dc2ba29d0e7b4ce 100644 (file)
@@ -7,10 +7,11 @@
  * @module  WoltLabSuite/Core/Ui/TabMenu/Simple
  */
 
-import * as DomTraverse from '../../Dom/Traverse';
-import DomUtil from '../../Dom/Util';
-import * as Environment from '../../Environment';
-import * as EventHandler from '../../Event/Handler';
+import * as Core from "../../Core";
+import * as DomTraverse from "../../Dom/Traverse";
+import DomUtil from "../../Dom/Util";
+import * as Environment from "../../Environment";
+import * as EventHandler from "../../Event/Handler";
 
 class TabMenuSimple {
   private readonly container: HTMLElement;
@@ -19,7 +20,7 @@ class TabMenuSimple {
   private store: HTMLInputElement | null = null;
   private readonly tabs = new Map<string, HTMLLIElement>();
 
-  constructor(container) {
+  constructor(container: HTMLElement) {
     this.container = container;
   }
 
@@ -38,22 +39,22 @@ class TabMenuSimple {
    * </div>
    */
   validate(): boolean {
-    if (!this.container.classList.contains('tabMenuContainer')) {
+    if (!this.container.classList.contains("tabMenuContainer")) {
       return false;
     }
 
-    const nav = DomTraverse.childByTag(this.container, 'NAV') as HTMLElement;
+    const nav = DomTraverse.childByTag(this.container, "NAV");
     if (nav === null) {
       return false;
     }
 
     // get children
-    const tabs = nav.querySelectorAll('li');
+    const tabs = nav.querySelectorAll("li");
     if (tabs.length === 0) {
       return false;
     }
 
-    DomTraverse.childrenByTag(this.container, 'DIV').forEach((container: HTMLElement) => {
+    DomTraverse.childrenByTag(this.container, "DIV").forEach((container) => {
       let name = container.dataset.name;
       if (!name) {
         name = DomUtil.identify(container);
@@ -64,26 +65,38 @@ class TabMenuSimple {
     });
 
     const containerId = this.container.id;
-    tabs.forEach(tab => {
+    tabs.forEach((tab) => {
       const name = this._getTabName(tab);
       if (!name) {
         return;
       }
 
       if (this.tabs.has(name)) {
-        throw new Error("Tab names must be unique, li[data-name='" + name + "'] (tab menu id: '" + containerId + "') exists more than once.");
+        throw new Error(
+          "Tab names must be unique, li[data-name='" +
+            name +
+            "'] (tab menu id: '" +
+            containerId +
+            "') exists more than once.",
+        );
       }
 
       const container = this.containers.get(name);
       if (container === undefined) {
-        throw new Error("Expected content element for li[data-name='" + name + "'] (tab menu id: '" + containerId + "').");
+        throw new Error(
+          "Expected content element for li[data-name='" + name + "'] (tab menu id: '" + containerId + "').",
+        );
       } else if (container.parentNode !== this.container) {
-        throw new Error("Expected content element '" + name + "' (tab menu id: '" + containerId + "') to be a direct children.");
+        throw new Error(
+          "Expected content element '" + name + "' (tab menu id: '" + containerId + "') to be a direct children.",
+        );
       }
 
       // check if tab holds exactly one children which is an anchor element
-      if (tab.childElementCount !== 1 || tab.children[0].nodeName !== 'A') {
-        throw new Error("Expected exactly one <a> as children for li[data-name='" + name + "'] (tab menu id: '" + containerId + "').");
+      if (tab.childElementCount !== 1 || tab.children[0].nodeName !== "A") {
+        throw new Error(
+          "Expected exactly one <a> as children for li[data-name='" + name + "'] (tab menu id: '" + containerId + "').",
+        );
       }
 
       this.tabs.set(name, tab);
@@ -94,10 +107,10 @@ class TabMenuSimple {
     }
 
     if (this.isLegacy) {
-      this.container.dataset.isLegacy = 'true';
+      this.container.dataset.isLegacy = "true";
 
       this.tabs.forEach(function (tab, name) {
-        tab.setAttribute('aria-controls', name);
+        tab.setAttribute("aria-controls", name);
       });
     }
 
@@ -109,30 +122,30 @@ class TabMenuSimple {
    */
   init(oldTabs?: Map<string, HTMLLIElement> | null): HTMLElement | null {
     // bind listeners
-    this.tabs.forEach(tab => {
-      if (!oldTabs || oldTabs.get(tab.dataset.name || '') !== tab) {
+    this.tabs.forEach((tab) => {
+      if (!oldTabs || oldTabs.get(tab.dataset.name || "") !== tab) {
         const firstChild = tab.children[0] as HTMLElement;
-        firstChild.addEventListener('click', (ev) => this._onClick(ev));
+        firstChild.addEventListener("click", (ev) => this._onClick(ev));
 
         // iOS 13 changed the behavior for click events after scrolling the menu. It prevents
         // the synthetic mouse events like "click" from triggering for a short duration after
         // a scrolling has occurred. If the user scrolls to the end of the list and immediately
         // attempts to click the tab, nothing will happen. However, if the user waits for some
         // time, the tap will trigger a "click" event again.
-        // 
+        //
         // A "click" event is basically the result of a touch without any (significant) finger
         // movement indicated by a "touchmove" event. This changes allows the user to scroll
         // both the menu and the page normally, but still benefit from snappy reactions when
         // tapping a menu item.
-        if (Environment.platform() === 'ios') {
+        if (Environment.platform() === "ios") {
           let isClick = false;
-          firstChild.addEventListener('touchstart', () => {
+          firstChild.addEventListener("touchstart", () => {
             isClick = true;
           });
-          firstChild.addEventListener('touchmove', () => {
+          firstChild.addEventListener("touchmove", () => {
             isClick = false;
           });
-          firstChild.addEventListener('touchend', (event) => {
+          firstChild.addEventListener("touchend", (event) => {
             if (isClick) {
               isClick = false;
 
@@ -151,13 +164,13 @@ class TabMenuSimple {
     if (!oldTabs) {
       const hash = TabMenuSimple.getIdentifierFromHash();
       let selectTab: HTMLLIElement | undefined = undefined;
-      if (hash !== '') {
+      if (hash !== "") {
         selectTab = this.tabs.get(hash);
 
         // check for parent tab menu
         if (selectTab) {
           const item = this.container.parentNode as HTMLElement;
-          if (item.classList.contains('tabMenuContainer')) {
+          if (item.classList.contains("tabMenuContainer")) {
             returnValue = item;
           }
         }
@@ -171,18 +184,22 @@ class TabMenuSimple {
 
         if (preselect === true) {
           this.tabs.forEach(function (tab) {
-            if (!selectTab && !DomUtil.isHidden(tab) && (!tab.previousElementSibling || DomUtil.isHidden(tab.previousElementSibling as HTMLElement))) {
+            if (
+              !selectTab &&
+              !DomUtil.isHidden(tab) &&
+              (!tab.previousElementSibling || DomUtil.isHidden(tab.previousElementSibling as HTMLElement))
+            ) {
               selectTab = tab;
             }
           });
-        } else if (typeof preselect === 'string' && preselect !== "false") {
+        } else if (typeof preselect === "string" && preselect !== "false") {
           selectTab = this.tabs.get(preselect);
         }
       }
 
       if (selectTab) {
-        this.containers.forEach(container => {
-          container.classList.add('hidden');
+        this.containers.forEach((container) => {
+          container.classList.add("hidden");
         });
 
         this.select(null, selectTab, true);
@@ -190,10 +207,10 @@ class TabMenuSimple {
 
       const store = this.container.dataset.store;
       if (store) {
-        const input = document.createElement('input');
-        input.type = 'hidden';
+        const input = document.createElement("input");
+        input.type = "hidden";
         input.name = store;
-        input.value = this.getActiveTab().dataset.name || '';
+        input.value = this.getActiveTab().dataset.name || "";
 
         this.container.appendChild(input);
 
@@ -212,7 +229,7 @@ class TabMenuSimple {
    * @param  {boolean=}    disableEvent  suppress event handling
    */
   select(name: number | string | null, tab?: HTMLLIElement, disableEvent?: boolean): void {
-    name = (name) ? name.toString() : '';
+    name = name ? name.toString() : "";
     tab = tab || this.tabs.get(name);
 
     if (!tab) {
@@ -221,7 +238,7 @@ class TabMenuSimple {
         name = ~~name;
 
         let i = 0;
-        this.tabs.forEach(item => {
+        this.tabs.forEach((item) => {
           if (i === name) {
             tab = item;
           }
@@ -231,11 +248,11 @@ class TabMenuSimple {
       }
 
       if (!tab) {
-        throw new Error("Expected a valid tab name, '" + name + "' given (tab menu id: '" + this.container.id + "').");
+        throw new Error(`Expected a valid tab name, '${name}' given (tab menu id: '${this.container.id}').`);
       }
     }
 
-    name = (name || tab.dataset.name || '') as string;
+    name = (name || tab.dataset.name || "") as string;
 
     // unmark active tab
     const oldTab = this.getActiveTab();
@@ -248,31 +265,31 @@ class TabMenuSimple {
       }
 
       if (!disableEvent) {
-        EventHandler.fire('com.woltlab.wcf.simpleTabMenu_' + this.container.id, 'beforeSelect', {
+        EventHandler.fire("com.woltlab.wcf.simpleTabMenu_" + this.container.id, "beforeSelect", {
           tab: oldTab,
           tabName: oldTabName,
         });
       }
 
-      oldTab.classList.remove('active');
-      oldContent = this.containers.get(oldTab.dataset.name || '')!;
-      oldContent.classList.remove('active');
-      oldContent.classList.add('hidden');
+      oldTab.classList.remove("active");
+      oldContent = this.containers.get(oldTab.dataset.name || "")!;
+      oldContent.classList.remove("active");
+      oldContent.classList.add("hidden");
 
       if (this.isLegacy) {
-        oldTab.classList.remove('ui-state-active');
-        oldContent.classList.remove('ui-state-active');
+        oldTab.classList.remove("ui-state-active");
+        oldContent.classList.remove("ui-state-active");
       }
     }
 
-    tab.classList.add('active');
+    tab.classList.add("active");
     const newContent = this.containers.get(name)!;
-    newContent.classList.add('active');
-    newContent.classList.remove('hidden');
+    newContent.classList.add("active");
+    newContent.classList.remove("hidden");
 
     if (this.isLegacy) {
-      tab.classList.add('ui-state-active');
-      newContent.classList.add('ui-state-active');
+      tab.classList.add("ui-state-active");
+      newContent.classList.add("ui-state-active");
     }
 
     if (this.store) {
@@ -280,46 +297,38 @@ class TabMenuSimple {
     }
 
     if (!disableEvent) {
-      EventHandler.fire('com.woltlab.wcf.simpleTabMenu_' + this.container.id, 'select', {
+      EventHandler.fire("com.woltlab.wcf.simpleTabMenu_" + this.container.id, "select", {
         active: tab,
         activeName: name,
         previous: oldTab,
         previousName: oldTab ? oldTab.dataset.name : null,
       });
 
-      const jQuery = (this.isLegacy && typeof window.jQuery === 'function') ? window.jQuery : null;
+      const jQuery = this.isLegacy && typeof window.jQuery === "function" ? window.jQuery : null;
       if (jQuery) {
         // simulate jQuery UI Tabs event
-        jQuery(this.container).trigger('wcftabsbeforeactivate', {
+        jQuery(this.container).trigger("wcftabsbeforeactivate", {
           newTab: jQuery(tab),
           oldTab: jQuery(oldTab),
           newPanel: jQuery(newContent),
-          oldPanel: jQuery(oldContent),
+          oldPanel: jQuery(oldContent!),
         });
       }
 
-      let location = window.location.href.replace(/#+[^#]*$/, '');
+      let location = window.location.href.replace(/#+[^#]*$/, "");
       if (TabMenuSimple.getIdentifierFromHash() === name) {
         location += window.location.hash;
       } else {
-        location += '#' + name;
+        location += "#" + name;
       }
 
       // update history
-      window.history.replaceState(
-        undefined,
-        '',
-        location,
-      );
+      window.history.replaceState(undefined, "", location);
     }
 
-    // TODO
-    /*
-    require(['WoltLabSuite/Core/Ui/TabMenu'], function (UiTabMenu) {
-      //noinspection JSUnresolvedFunction
-      UiTabMenu.scrollToTab(tab);
+    void import("../TabMenu").then((UiTabMenu) => {
+      UiTabMenu.scrollToTab(tab!);
     });
-     */
   }
 
   /**
@@ -331,7 +340,7 @@ class TabMenuSimple {
    */
   selectFirstVisible(): boolean {
     let selectTab: HTMLLIElement | null = null;
-    this.tabs.forEach(tab => {
+    this.tabs.forEach((tab) => {
       if (!selectTab && !DomUtil.isHidden(tab)) {
         selectTab = tab;
       }
@@ -382,9 +391,9 @@ class TabMenuSimple {
 
     // handle legacy tab menus
     if (!name) {
-      if (tab.childElementCount === 1 && tab.children[0].nodeName === 'A') {
+      if (tab.childElementCount === 1 && tab.children[0].nodeName === "A") {
         const link = tab.children[0] as HTMLAnchorElement;
-        if (link.href.match(/#([^#]+)$/)) {
+        if (/#([^#]+)$/.exec(link.href)) {
           name = RegExp.$1;
 
           if (document.getElementById(name) === null) {
@@ -404,7 +413,7 @@ class TabMenuSimple {
    * Returns the currently active tab.
    */
   getActiveTab(): HTMLLIElement {
-    return document.querySelector('#' + this.container.id + ' > nav > ul > li.active') as HTMLLIElement;
+    return document.querySelector("#" + this.container.id + " > nav > ul > li.active") as HTMLLIElement;
   }
 
   /**
@@ -421,14 +430,15 @@ class TabMenuSimple {
     return this.tabs;
   }
 
-  static getIdentifierFromHash() {
-    if (window.location.hash.match(/^#+([^\/]+)+(?:\/.+)?/)) {
+  static getIdentifierFromHash(): string {
+    if (/^#+([^/]+)+(?:\/.+)?/.exec(window.location.hash)) {
       return RegExp.$1;
     }
 
-    return '';
-  };
+    return "";
+  }
 }
 
+Core.enableLegacyInheritance(TabMenuSimple);
+
 export = TabMenuSimple;
-