Convert `Ui/FlexibleMenu` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Fri, 23 Oct 2020 19:02:27 +0000 (21:02 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Wed, 28 Oct 2020 11:57:20 +0000 (12:57 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Traverse.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/FlexibleMenu.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Dom/Traverse.ts
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Dropdown/Simple.ts
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/FlexibleMenu.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/FlexibleMenu.ts [new file with mode: 0644]

index 82e7a72887bf369d9dd223746219c8b389d393ca..d39bc40c7e184e0d033f73cb60126831ad201048 100644 (file)
@@ -6,6 +6,7 @@
  * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module  Dom/Traverse (alias)
  * @module  WoltLabSuite/Core/Dom/Traverse
+ * @module DomTraverse
  */
 define(["require", "exports"], function (require, exports) {
     "use strict";
index 69fff1bb7ec9de5108e031197f0c5e2a4b1c93c6..deae94e7a8c70df8fabbd96acc16d70e58796a72 100644 (file)
  * Dynamically transforms menu-like structures to handle items exceeding the available width
  * by moving them into a separate dropdown.
  *
- * @author     Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Ui/FlexibleMenu
+ * @author  Alexander Ebert
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/FlexibleMenu
  */
-define(['Core', 'Dictionary', 'Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util', 'Ui/SimpleDropdown'], function (Core, Dictionary, DomChangeListener, DomTraverse, DomUtil, SimpleDropdown) {
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+}) : (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+    Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+    o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+    if (mod && mod.__esModule) return mod;
+    var result = {};
+    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+    __setModuleDefault(result, mod);
+    return result;
+};
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+define(["require", "exports", "../Dom/Change/Listener", "../Dom/Util", "../Dom/Traverse", "./Dropdown/Simple"], function (require, exports, Listener_1, Util_1, DomTraverse, Simple_1) {
     "use strict";
-    var _containers = new Dictionary();
-    var _dropdowns = new Dictionary();
-    var _dropdownMenus = new Dictionary();
-    var _itemLists = new Dictionary();
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.rebuild = exports.rebuildAll = exports.registerTabMenus = exports.register = exports.setup = void 0;
+    Listener_1 = __importDefault(Listener_1);
+    Util_1 = __importDefault(Util_1);
+    DomTraverse = __importStar(DomTraverse);
+    Simple_1 = __importDefault(Simple_1);
+    const _containers = new Map();
+    const _dropdowns = new Map();
+    const _dropdownMenus = new Map();
+    const _itemLists = new Map();
     /**
-     * @exports        WoltLabSuite/Core/Ui/FlexibleMenu
+     * Register default menus and set up event listeners.
      */
-    var UiFlexibleMenu = {
-        /**
-         * Register default menus and set up event listeners.
-         */
-        setup: function () {
-            if (elById('mainMenu') !== null)
-                this.register('mainMenu');
-            var navigationHeader = elBySel('.navigationHeader');
-            if (navigationHeader !== null)
-                this.register(DomUtil.identify(navigationHeader));
-            window.addEventListener('resize', this.rebuildAll.bind(this));
-            DomChangeListener.add('WoltLabSuite/Core/Ui/FlexibleMenu', this.registerTabMenus.bind(this));
-        },
-        /**
-         * Registers a menu by element id.
-         *
-         * @param      {string}        containerId     element id
-         */
-        register: function (containerId) {
-            var container = elById(containerId);
-            if (container === null) {
-                throw "Expected a valid element id, '" + containerId + "' does not exist.";
-            }
-            if (_containers.has(containerId)) {
-                return;
-            }
-            var list = DomTraverse.childByTag(container, 'UL');
-            if (list === null) {
-                throw "Expected an <ul> element as child of container '" + containerId + "'.";
+    function setup() {
+        if (document.getElementById('mainMenu') !== null) {
+            register('mainMenu');
+        }
+        const navigationHeader = document.querySelector('.navigationHeader');
+        if (navigationHeader !== null) {
+            register(Util_1.default.identify(navigationHeader));
+        }
+        window.addEventListener('resize', rebuildAll);
+        Listener_1.default.add('WoltLabSuite/Core/Ui/FlexibleMenu', registerTabMenus);
+    }
+    exports.setup = setup;
+    /**
+     * Registers a menu by element id.
+     */
+    function register(containerId) {
+        const container = document.getElementById(containerId);
+        if (container === null) {
+            throw "Expected a valid element id, '" + containerId + "' does not exist.";
+        }
+        if (_containers.has(containerId)) {
+            return;
+        }
+        const list = DomTraverse.childByTag(container, 'UL');
+        if (list === null) {
+            throw "Expected an <ul> element as child of container '" + containerId + "'.";
+        }
+        _containers.set(containerId, container);
+        _itemLists.set(containerId, list);
+        rebuild(containerId);
+    }
+    exports.register = register;
+    /**
+     * Registers tab menus.
+     */
+    function registerTabMenus() {
+        document.querySelectorAll('.tabMenuContainer:not(.jsFlexibleMenuEnabled), .messageTabMenu:not(.jsFlexibleMenuEnabled)').forEach(tabMenu => {
+            const nav = DomTraverse.childByTag(tabMenu, 'NAV');
+            if (nav !== null) {
+                tabMenu.classList.add('jsFlexibleMenuEnabled');
+                register(Util_1.default.identify(nav));
             }
-            _containers.set(containerId, container);
-            _itemLists.set(containerId, list);
-            this.rebuild(containerId);
-        },
-        /**
-         * Registers tab menus.
-         */
-        registerTabMenus: function () {
-            var tabMenus = elBySelAll('.tabMenuContainer:not(.jsFlexibleMenuEnabled), .messageTabMenu:not(.jsFlexibleMenuEnabled)');
-            for (var i = 0, length = tabMenus.length; i < length; i++) {
-                var tabMenu = tabMenus[i];
-                var nav = DomTraverse.childByTag(tabMenu, 'NAV');
-                if (nav !== null) {
-                    tabMenu.classList.add('jsFlexibleMenuEnabled');
-                    this.register(DomUtil.identify(nav));
+        });
+    }
+    exports.registerTabMenus = registerTabMenus;
+    /**
+     * Rebuilds all menus, e.g. on window resize.
+     */
+    function rebuildAll() {
+        _containers.forEach((container, containerId) => {
+            rebuild(containerId);
+        });
+    }
+    exports.rebuildAll = rebuildAll;
+    /**
+     * Rebuild the menu identified by given element id.
+     */
+    function rebuild(containerId) {
+        const container = _containers.get(containerId);
+        if (container === undefined) {
+            throw "Expected a valid element id, '" + containerId + "' is unknown.";
+        }
+        const styles = window.getComputedStyle(container);
+        const parent = container.parentNode;
+        let availableWidth = parent.clientWidth;
+        availableWidth -= Util_1.default.styleAsInt(styles, 'margin-left');
+        availableWidth -= Util_1.default.styleAsInt(styles, 'margin-right');
+        const list = _itemLists.get(containerId);
+        const items = DomTraverse.childrenByTag(list, 'LI');
+        let dropdown = _dropdowns.get(containerId);
+        let dropdownWidth = 0;
+        if (dropdown !== undefined) {
+            // show all items for calculation
+            for (let i = 0, length = items.length; i < length; i++) {
+                const item = items[i];
+                if (item.classList.contains('dropdown')) {
+                    continue;
                 }
+                Util_1.default.show(item);
             }
-        },
-        /**
-         * Rebuilds all menus, e.g. on window resize.
-         */
-        rebuildAll: function () {
-            _containers.forEach((function (container, containerId) {
-                this.rebuild(containerId);
-            }).bind(this));
-        },
-        /**
-         * Rebuild the menu identified by given element id.
-         *
-         * @param      {string}        containerId     element id
-         */
-        rebuild: function (containerId) {
-            var container = _containers.get(containerId);
-            if (container === undefined) {
-                throw "Expected a valid element id, '" + containerId + "' is unknown.";
+            if (dropdown.parentNode !== null) {
+                dropdownWidth = Util_1.default.outerWidth(dropdown);
             }
-            var styles = window.getComputedStyle(container);
-            var availableWidth = container.parentNode.clientWidth;
-            availableWidth -= DomUtil.styleAsInt(styles, 'margin-left');
-            availableWidth -= DomUtil.styleAsInt(styles, 'margin-right');
-            var list = _itemLists.get(containerId);
-            var items = DomTraverse.childrenByTag(list, 'LI');
-            var dropdown = _dropdowns.get(containerId);
-            var dropdownWidth = 0;
-            if (dropdown !== undefined) {
-                // show all items for calculation
-                for (var i = 0, length = items.length; i < length; i++) {
-                    var item = items[i];
-                    if (item.classList.contains('dropdown')) {
-                        continue;
-                    }
-                    elShow(item);
+        }
+        const currentWidth = list.scrollWidth - dropdownWidth;
+        const hiddenItems = [];
+        if (currentWidth > availableWidth) {
+            // hide items starting with the last one
+            for (let i = items.length - 1; i >= 0; i--) {
+                const item = items[i];
+                // ignore dropdown and active item
+                if (item.classList.contains('dropdown') || item.classList.contains('active') || item.classList.contains('ui-state-active')) {
+                    continue;
                 }
-                if (dropdown.parentNode !== null) {
-                    dropdownWidth = DomUtil.outerWidth(dropdown);
+                hiddenItems.push(item);
+                Util_1.default.hide(item);
+                if (list.scrollWidth < availableWidth) {
+                    break;
                 }
             }
-            var currentWidth = list.scrollWidth - dropdownWidth;
-            var hiddenItems = [];
-            if (currentWidth > availableWidth) {
-                // hide items starting with the last one
-                for (var i = items.length - 1; i >= 0; i--) {
-                    var item = items[i];
-                    // ignore dropdown and active item
-                    if (item.classList.contains('dropdown') || item.classList.contains('active') || item.classList.contains('ui-state-active')) {
-                        continue;
-                    }
-                    hiddenItems.push(item);
-                    elHide(item);
-                    if (list.scrollWidth < availableWidth) {
-                        break;
-                    }
-                }
+        }
+        if (hiddenItems.length) {
+            let dropdownMenu;
+            if (dropdown === undefined) {
+                dropdown = document.createElement('li');
+                dropdown.className = 'dropdown jsFlexibleMenuDropdown';
+                const icon = document.createElement('a');
+                icon.className = 'icon icon16 fa-list';
+                dropdown.appendChild(icon);
+                dropdownMenu = document.createElement('ul');
+                dropdownMenu.classList.add('dropdownMenu');
+                dropdown.appendChild(dropdownMenu);
+                _dropdowns.set(containerId, dropdown);
+                _dropdownMenus.set(containerId, dropdownMenu);
+                Simple_1.default.init(icon);
             }
-            if (hiddenItems.length) {
-                var dropdownMenu;
-                if (dropdown === undefined) {
-                    dropdown = elCreate('li');
-                    dropdown.className = 'dropdown jsFlexibleMenuDropdown';
-                    var icon = elCreate('a');
-                    icon.className = 'icon icon16 fa-list';
-                    dropdown.appendChild(icon);
-                    dropdownMenu = elCreate('ul');
-                    dropdownMenu.classList.add('dropdownMenu');
-                    dropdown.appendChild(dropdownMenu);
-                    _dropdowns.set(containerId, dropdown);
-                    _dropdownMenus.set(containerId, dropdownMenu);
-                    SimpleDropdown.init(icon);
-                }
-                else {
-                    dropdownMenu = _dropdownMenus.get(containerId);
-                }
-                if (dropdown.parentNode === null) {
-                    list.appendChild(dropdown);
-                }
-                // build dropdown menu
-                var fragment = document.createDocumentFragment();
-                var self = this;
-                hiddenItems.forEach(function (hiddenItem) {
-                    var item = elCreate('li');
-                    item.innerHTML = hiddenItem.innerHTML;
-                    item.addEventListener(WCF_CLICK_EVENT, (function (event) {
-                        event.preventDefault();
-                        Core.triggerEvent(elBySel('a', hiddenItem), WCF_CLICK_EVENT);
-                        // force a rebuild to guarantee the active item being visible
-                        setTimeout(function () {
-                            self.rebuild(containerId);
-                        }, 59);
-                    }).bind(this));
-                    fragment.appendChild(item);
-                });
-                dropdownMenu.innerHTML = '';
-                dropdownMenu.appendChild(fragment);
+            else {
+                dropdownMenu = _dropdownMenus.get(containerId);
             }
-            else if (dropdown !== undefined && dropdown.parentNode !== null) {
-                elRemove(dropdown);
+            if (dropdown.parentNode === null) {
+                list.appendChild(dropdown);
             }
+            // build dropdown menu
+            const fragment = document.createDocumentFragment();
+            hiddenItems.forEach(hiddenItem => {
+                const item = document.createElement('li');
+                item.innerHTML = hiddenItem.innerHTML;
+                item.addEventListener('click', event => {
+                    var _a;
+                    event.preventDefault();
+                    (_a = hiddenItem.querySelector('a')) === null || _a === void 0 ? void 0 : _a.click();
+                    // force a rebuild to guarantee the active item being visible
+                    setTimeout(() => {
+                        rebuild(containerId);
+                    }, 59);
+                });
+                fragment.appendChild(item);
+            });
+            dropdownMenu.innerHTML = '';
+            dropdownMenu.appendChild(fragment);
+        }
+        else if (dropdown !== undefined && dropdown.parentNode !== null) {
+            dropdown.remove();
         }
-    };
-    return UiFlexibleMenu;
+    }
+    exports.rebuild = rebuild;
 });
index 4ae5e7cf7f5f5ce013e77efb90bc95ca6085e326..34c8ed9e83c6b86a8afffc2162cd0d76ab530876 100644 (file)
@@ -6,6 +6,7 @@
  * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module  Dom/Traverse (alias)
  * @module  WoltLabSuite/Core/Dom/Traverse
+ * @module DomTraverse
  */
 
 const enum Type {
index 0f57cbb38b95addd46d8ae667c5b136114011eb0..7ebe4b433820db7fb821094ccd53d8a33f72dfa9 100644 (file)
@@ -362,7 +362,7 @@ const UiDropdownSimple = {
   /**
    * Initializes a dropdown.
    */
-  init(button: HTMLElement, isLazyInitialization: boolean | MouseEvent): boolean {
+  init(button: HTMLElement, isLazyInitialization?: boolean | MouseEvent): boolean {
     UiDropdownSimple.setup();
 
     button.setAttribute('role', 'button');
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/FlexibleMenu.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/FlexibleMenu.js
deleted file mode 100644 (file)
index 69fff1b..0000000
+++ /dev/null
@@ -1,169 +0,0 @@
-/**
- * Dynamically transforms menu-like structures to handle items exceeding the available width
- * by moving them into a separate dropdown.
- *
- * @author     Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Ui/FlexibleMenu
- */
-define(['Core', 'Dictionary', 'Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util', 'Ui/SimpleDropdown'], function (Core, Dictionary, DomChangeListener, DomTraverse, DomUtil, SimpleDropdown) {
-    "use strict";
-    var _containers = new Dictionary();
-    var _dropdowns = new Dictionary();
-    var _dropdownMenus = new Dictionary();
-    var _itemLists = new Dictionary();
-    /**
-     * @exports        WoltLabSuite/Core/Ui/FlexibleMenu
-     */
-    var UiFlexibleMenu = {
-        /**
-         * Register default menus and set up event listeners.
-         */
-        setup: function () {
-            if (elById('mainMenu') !== null)
-                this.register('mainMenu');
-            var navigationHeader = elBySel('.navigationHeader');
-            if (navigationHeader !== null)
-                this.register(DomUtil.identify(navigationHeader));
-            window.addEventListener('resize', this.rebuildAll.bind(this));
-            DomChangeListener.add('WoltLabSuite/Core/Ui/FlexibleMenu', this.registerTabMenus.bind(this));
-        },
-        /**
-         * Registers a menu by element id.
-         *
-         * @param      {string}        containerId     element id
-         */
-        register: function (containerId) {
-            var container = elById(containerId);
-            if (container === null) {
-                throw "Expected a valid element id, '" + containerId + "' does not exist.";
-            }
-            if (_containers.has(containerId)) {
-                return;
-            }
-            var list = DomTraverse.childByTag(container, 'UL');
-            if (list === null) {
-                throw "Expected an <ul> element as child of container '" + containerId + "'.";
-            }
-            _containers.set(containerId, container);
-            _itemLists.set(containerId, list);
-            this.rebuild(containerId);
-        },
-        /**
-         * Registers tab menus.
-         */
-        registerTabMenus: function () {
-            var tabMenus = elBySelAll('.tabMenuContainer:not(.jsFlexibleMenuEnabled), .messageTabMenu:not(.jsFlexibleMenuEnabled)');
-            for (var i = 0, length = tabMenus.length; i < length; i++) {
-                var tabMenu = tabMenus[i];
-                var nav = DomTraverse.childByTag(tabMenu, 'NAV');
-                if (nav !== null) {
-                    tabMenu.classList.add('jsFlexibleMenuEnabled');
-                    this.register(DomUtil.identify(nav));
-                }
-            }
-        },
-        /**
-         * Rebuilds all menus, e.g. on window resize.
-         */
-        rebuildAll: function () {
-            _containers.forEach((function (container, containerId) {
-                this.rebuild(containerId);
-            }).bind(this));
-        },
-        /**
-         * Rebuild the menu identified by given element id.
-         *
-         * @param      {string}        containerId     element id
-         */
-        rebuild: function (containerId) {
-            var container = _containers.get(containerId);
-            if (container === undefined) {
-                throw "Expected a valid element id, '" + containerId + "' is unknown.";
-            }
-            var styles = window.getComputedStyle(container);
-            var availableWidth = container.parentNode.clientWidth;
-            availableWidth -= DomUtil.styleAsInt(styles, 'margin-left');
-            availableWidth -= DomUtil.styleAsInt(styles, 'margin-right');
-            var list = _itemLists.get(containerId);
-            var items = DomTraverse.childrenByTag(list, 'LI');
-            var dropdown = _dropdowns.get(containerId);
-            var dropdownWidth = 0;
-            if (dropdown !== undefined) {
-                // show all items for calculation
-                for (var i = 0, length = items.length; i < length; i++) {
-                    var item = items[i];
-                    if (item.classList.contains('dropdown')) {
-                        continue;
-                    }
-                    elShow(item);
-                }
-                if (dropdown.parentNode !== null) {
-                    dropdownWidth = DomUtil.outerWidth(dropdown);
-                }
-            }
-            var currentWidth = list.scrollWidth - dropdownWidth;
-            var hiddenItems = [];
-            if (currentWidth > availableWidth) {
-                // hide items starting with the last one
-                for (var i = items.length - 1; i >= 0; i--) {
-                    var item = items[i];
-                    // ignore dropdown and active item
-                    if (item.classList.contains('dropdown') || item.classList.contains('active') || item.classList.contains('ui-state-active')) {
-                        continue;
-                    }
-                    hiddenItems.push(item);
-                    elHide(item);
-                    if (list.scrollWidth < availableWidth) {
-                        break;
-                    }
-                }
-            }
-            if (hiddenItems.length) {
-                var dropdownMenu;
-                if (dropdown === undefined) {
-                    dropdown = elCreate('li');
-                    dropdown.className = 'dropdown jsFlexibleMenuDropdown';
-                    var icon = elCreate('a');
-                    icon.className = 'icon icon16 fa-list';
-                    dropdown.appendChild(icon);
-                    dropdownMenu = elCreate('ul');
-                    dropdownMenu.classList.add('dropdownMenu');
-                    dropdown.appendChild(dropdownMenu);
-                    _dropdowns.set(containerId, dropdown);
-                    _dropdownMenus.set(containerId, dropdownMenu);
-                    SimpleDropdown.init(icon);
-                }
-                else {
-                    dropdownMenu = _dropdownMenus.get(containerId);
-                }
-                if (dropdown.parentNode === null) {
-                    list.appendChild(dropdown);
-                }
-                // build dropdown menu
-                var fragment = document.createDocumentFragment();
-                var self = this;
-                hiddenItems.forEach(function (hiddenItem) {
-                    var item = elCreate('li');
-                    item.innerHTML = hiddenItem.innerHTML;
-                    item.addEventListener(WCF_CLICK_EVENT, (function (event) {
-                        event.preventDefault();
-                        Core.triggerEvent(elBySel('a', hiddenItem), WCF_CLICK_EVENT);
-                        // force a rebuild to guarantee the active item being visible
-                        setTimeout(function () {
-                            self.rebuild(containerId);
-                        }, 59);
-                    }).bind(this));
-                    fragment.appendChild(item);
-                });
-                dropdownMenu.innerHTML = '';
-                dropdownMenu.appendChild(fragment);
-            }
-            else if (dropdown !== undefined && dropdown.parentNode !== null) {
-                elRemove(dropdown);
-            }
-        }
-    };
-    return UiFlexibleMenu;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/FlexibleMenu.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/FlexibleMenu.ts
new file mode 100644 (file)
index 0000000..fba1e07
--- /dev/null
@@ -0,0 +1,189 @@
+/**
+ * Dynamically transforms menu-like structures to handle items exceeding the available width
+ * by moving them into a separate dropdown.
+ *
+ * @author  Alexander Ebert
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/FlexibleMenu
+ */
+
+import DomChangeListener from '../Dom/Change/Listener';
+import DomUtil from '../Dom/Util';
+import * as DomTraverse from '../Dom/Traverse';
+import UiDropdownSimple from './Dropdown/Simple';
+
+const _containers = new Map<string, HTMLElement>();
+const _dropdowns = new Map<string, HTMLLIElement>();
+const _dropdownMenus = new Map<string, HTMLUListElement>();
+const _itemLists = new Map<string, HTMLUListElement>();
+
+/**
+ * Register default menus and set up event listeners.
+ */
+export function setup(): void {
+  if (document.getElementById('mainMenu') !== null) {
+    register('mainMenu');
+  }
+
+  const navigationHeader = document.querySelector('.navigationHeader');
+  if (navigationHeader !== null) {
+    register(DomUtil.identify(navigationHeader));
+  }
+
+  window.addEventListener('resize', rebuildAll);
+  DomChangeListener.add('WoltLabSuite/Core/Ui/FlexibleMenu', registerTabMenus);
+}
+
+/**
+ * Registers a menu by element id.
+ */
+export function register(containerId: string): void {
+  const container = document.getElementById(containerId);
+  if (container === null) {
+    throw "Expected a valid element id, '" + containerId + "' does not exist.";
+  }
+
+  if (_containers.has(containerId)) {
+    return;
+  }
+
+  const list = DomTraverse.childByTag(container, 'UL') as HTMLUListElement;
+  if (list === null) {
+    throw "Expected an <ul> element as child of container '" + containerId + "'.";
+  }
+
+  _containers.set(containerId, container);
+  _itemLists.set(containerId, list);
+
+  rebuild(containerId);
+}
+
+/**
+ * Registers tab menus.
+ */
+export function registerTabMenus(): void {
+  document.querySelectorAll('.tabMenuContainer:not(.jsFlexibleMenuEnabled), .messageTabMenu:not(.jsFlexibleMenuEnabled)').forEach(tabMenu => {
+    const nav = DomTraverse.childByTag(tabMenu, 'NAV');
+    if (nav !== null) {
+      tabMenu.classList.add('jsFlexibleMenuEnabled');
+      register(DomUtil.identify(nav));
+    }
+  });
+}
+
+/**
+ * Rebuilds all menus, e.g. on window resize.
+ */
+export function rebuildAll(): void {
+  _containers.forEach((container, containerId) => {
+    rebuild(containerId);
+  });
+}
+
+/**
+ * Rebuild the menu identified by given element id.
+ */
+export function rebuild(containerId: string): void {
+  const container = _containers.get(containerId);
+  if (container === undefined) {
+    throw "Expected a valid element id, '" + containerId + "' is unknown.";
+  }
+
+  const styles = window.getComputedStyle(container);
+  const parent = container.parentNode as HTMLElement;
+  let availableWidth = parent.clientWidth;
+  availableWidth -= DomUtil.styleAsInt(styles, 'margin-left');
+  availableWidth -= DomUtil.styleAsInt(styles, 'margin-right');
+
+  const list = _itemLists.get(containerId)!;
+  const items = DomTraverse.childrenByTag(list, 'LI') as HTMLLIElement[];
+  let dropdown = _dropdowns.get(containerId);
+  let dropdownWidth = 0;
+  if (dropdown !== undefined) {
+    // show all items for calculation
+    for (let i = 0, length = items.length; i < length; i++) {
+      const item = items[i];
+      if (item.classList.contains('dropdown')) {
+        continue;
+      }
+
+      DomUtil.show(item);
+    }
+    if (dropdown.parentNode !== null) {
+      dropdownWidth = DomUtil.outerWidth(dropdown);
+    }
+  }
+
+  const currentWidth = list.scrollWidth - dropdownWidth;
+  const hiddenItems: HTMLLIElement[] = [];
+  if (currentWidth > availableWidth) {
+    // hide items starting with the last one
+    for (let i = items.length - 1; i >= 0; i--) {
+      const item = items[i];
+
+      // ignore dropdown and active item
+      if (item.classList.contains('dropdown') || item.classList.contains('active') || item.classList.contains('ui-state-active')) {
+        continue;
+      }
+
+      hiddenItems.push(item);
+      DomUtil.hide(item);
+
+      if (list.scrollWidth < availableWidth) {
+        break;
+      }
+    }
+  }
+
+  if (hiddenItems.length) {
+    let dropdownMenu: HTMLUListElement;
+    if (dropdown === undefined) {
+      dropdown = document.createElement('li');
+      dropdown.className = 'dropdown jsFlexibleMenuDropdown';
+
+      const icon = document.createElement('a');
+      icon.className = 'icon icon16 fa-list';
+      dropdown.appendChild(icon);
+
+      dropdownMenu = document.createElement('ul');
+      dropdownMenu.classList.add('dropdownMenu');
+      dropdown.appendChild(dropdownMenu);
+
+      _dropdowns.set(containerId, dropdown);
+      _dropdownMenus.set(containerId, dropdownMenu);
+      UiDropdownSimple.init(icon);
+    } else {
+      dropdownMenu = _dropdownMenus.get(containerId)!;
+    }
+
+    if (dropdown.parentNode === null) {
+      list.appendChild(dropdown);
+    }
+
+    // build dropdown menu
+    const fragment = document.createDocumentFragment();
+    hiddenItems.forEach(hiddenItem => {
+      const item = document.createElement('li');
+      item.innerHTML = hiddenItem.innerHTML;
+
+      item.addEventListener('click', event => {
+        event.preventDefault();
+
+        hiddenItem.querySelector('a')?.click();
+
+        // force a rebuild to guarantee the active item being visible
+        setTimeout(() => {
+          rebuild(containerId);
+        }, 59);
+      });
+
+      fragment.appendChild(item);
+    });
+
+    dropdownMenu.innerHTML = '';
+    dropdownMenu.appendChild(fragment);
+  } else if (dropdown !== undefined && dropdown.parentNode !== null) {
+    dropdown.remove();
+  }
+}