Convert `Ui/Page/Header/Menu` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Wed, 28 Oct 2020 19:32:52 +0000 (20:32 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 28 Oct 2020 19:32:52 +0000 (20:32 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Header/Fixed.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Header/Menu.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Page/Header/Fixed.ts
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Page/Header/Menu.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Page/Header/Menu.ts [new file with mode: 0644]

index c38d0d439c305e6c3e3839ef1c2c48fdbb93e4e6..36583e54f005743b66362146d38d874a5ad8db51 100644 (file)
@@ -25,8 +25,6 @@ define(["require", "exports", "tslib", "../../../Event/Handler", "../../Alignmen
     let _userPanelSearchButton;
     /**
      * Provides the collapsible search bar.
-     *
-     * @protected
      */
     function initSearchBar() {
         _pageHeaderSearch = document.getElementById('pageHeaderSearch');
@@ -60,8 +58,6 @@ define(["require", "exports", "tslib", "../../../Event/Handler", "../../Alignmen
     }
     /**
      * Opens the search bar.
-     *
-     * @protected
      */
     function openSearchBar() {
         window.WCF.Dropdown.Interactive.Handler.closeAll();
@@ -81,8 +77,6 @@ define(["require", "exports", "tslib", "../../../Event/Handler", "../../Alignmen
     }
     /**
      * Closes the search bar.
-     *
-     * @protected
      */
     function closeSearchBar() {
         _pageHeader.classList.remove('searchBarOpen');
index 4b9b59ed726f0210da44060b40e91a6f839790b2..1be3d7b579abc8d1aaa86ee8027dc6ca9079eea8 100644 (file)
 /**
  * Handles main menu overflow and a11y.
  *
- * @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/Page/Header/Menu
+ * @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/Page/Header/Menu
  */
-define(['Environment', 'Language', 'Ui/Screen'], function (Environment, Language, UiScreen) {
+define(["require", "exports", "tslib", "../../../Environment", "../../../Language", "../../Screen"], function (require, exports, tslib_1, Environment, Language, UiScreen) {
     "use strict";
-    var _enabled = false;
-    // elements
-    var _buttonShowNext, _buttonShowPrevious, _firstElement, _menu;
-    // internal states
-    var _marginLeft = 0, _invisibleLeft = [], _invisibleRight = [];
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.init = void 0;
+    Environment = tslib_1.__importStar(Environment);
+    Language = tslib_1.__importStar(Language);
+    UiScreen = tslib_1.__importStar(UiScreen);
+    let _enabled = false;
+    let _buttonShowNext;
+    let _buttonShowPrevious;
+    let _firstElement;
+    let _menu;
+    let _marginLeft = 0;
+    let _invisibleLeft = [];
+    let _invisibleRight = [];
     /**
-     * @exports     WoltLabSuite/Core/Ui/Page/Header/Menu
+     * Enables the overflow handler.
      */
-    return {
-        /**
-         * Initializes the main menu overflow handling.
-         */
-        init: function () {
-            _menu = elBySel('.mainMenu .boxMenu');
-            _firstElement = (_menu && _menu.childElementCount) ? _menu.children[0] : null;
-            if (_firstElement === null) {
-                throw new Error("Unable to find the menu.");
-            }
-            UiScreen.on('screen-lg', {
-                enable: this._enable.bind(this),
-                disable: this._disable.bind(this),
-                setup: this._setup.bind(this)
-            });
-        },
-        /**
-         * Enables the overflow handler.
-         *
-         * @protected
-         */
-        _enable: function () {
-            _enabled = true;
-            // Safari waits three seconds for a font to be loaded which causes the header menu items
-            // to be extremely wide while waiting for the font to be loaded. The extremely wide menu
-            // items in turn can cause the overflow controls to be shown even if the width of the header
-            // menu, after the font has been loaded successfully, does not require them. This width
-            // issue results in the next button being shown for a short time. To circumvent this issue,
-            // we wait a second before showing the obverflow controls in Safari.
-            // see https://webkit.org/blog/6643/improved-font-loading/
-            if (Environment.browser() === 'safari') {
-                window.setTimeout(this._rebuildVisibility.bind(this), 1000);
-            }
-            else {
-                this._rebuildVisibility();
-                // IE11 sometimes suffers from a timing issue
-                window.setTimeout(this._rebuildVisibility.bind(this), 1000);
+    function enable() {
+        _enabled = true;
+        // Safari waits three seconds for a font to be loaded which causes the header menu items
+        // to be extremely wide while waiting for the font to be loaded. The extremely wide menu
+        // items in turn can cause the overflow controls to be shown even if the width of the header
+        // menu, after the font has been loaded successfully, does not require them. This width
+        // issue results in the next button being shown for a short time. To circumvent this issue,
+        // we wait a second before showing the obverflow controls in Safari.
+        // see https://webkit.org/blog/6643/improved-font-loading/
+        if (Environment.browser() === 'safari') {
+            window.setTimeout(rebuildVisibility, 1000);
+        }
+        else {
+            rebuildVisibility();
+            // IE11 sometimes suffers from a timing issue
+            window.setTimeout(rebuildVisibility, 1000);
+        }
+    }
+    /**
+     * Disables the overflow handler.
+     */
+    function disable() {
+        _enabled = false;
+    }
+    /**
+     * Displays the next three menu items.
+     */
+    function showNext(event) {
+        event.preventDefault();
+        if (_invisibleRight.length) {
+            const showItem = _invisibleRight.slice(0, 3).pop();
+            setMarginLeft(_menu.clientWidth - (showItem.offsetLeft + showItem.clientWidth));
+            if (_menu.lastElementChild === showItem) {
+                _buttonShowNext.classList.remove('active');
             }
-        },
-        /**
-         * Disables the overflow handler.
-         *
-         * @protected
-         */
-        _disable: function () {
-            _enabled = false;
-        },
-        /**
-         * Displays the next three menu items.
-         *
-         * @param       {Event}         event           event object
-         * @protected
-         */
-        _showNext: function (event) {
-            event.preventDefault();
-            if (_invisibleRight.length) {
-                var showItem = _invisibleRight.slice(0, 3).pop();
-                this._setMarginLeft(_menu.clientWidth - (showItem.offsetLeft + showItem.clientWidth));
-                if (_menu.lastElementChild === showItem) {
-                    _buttonShowNext.classList.remove('active');
-                }
-                _buttonShowPrevious.classList.add('active');
+            _buttonShowPrevious.classList.add('active');
+        }
+    }
+    /**
+     * Displays the previous three menu items.
+     */
+    function showPrevious(event) {
+        event.preventDefault();
+        if (_invisibleLeft.length) {
+            const showItem = _invisibleLeft.slice(-3)[0];
+            setMarginLeft(showItem.offsetLeft * -1);
+            if (_menu.firstElementChild === showItem) {
+                _buttonShowPrevious.classList.remove('active');
             }
-        },
-        /**
-         * Displays the previous three menu items.
-         *
-         * @param       {Event}         event           event object
-         * @protected
-         */
-        _showPrevious: function (event) {
-            event.preventDefault();
-            if (_invisibleLeft.length) {
-                var showItem = _invisibleLeft.slice(-3)[0];
-                this._setMarginLeft(showItem.offsetLeft * -1);
-                if (_menu.firstElementChild === showItem) {
-                    _buttonShowPrevious.classList.remove('active');
+            _buttonShowNext.classList.add('active');
+        }
+    }
+    /**
+     * Sets the first item's margin-left value that is
+     * used to move the menu contents around.
+     */
+    function setMarginLeft(offset) {
+        _marginLeft = Math.min(_marginLeft + offset, 0);
+        _firstElement.style.setProperty('margin-left', _marginLeft + 'px', '');
+    }
+    /**
+     * Toggles button overlays and rebuilds the list
+     * of invisible items from left to right.
+     */
+    function rebuildVisibility() {
+        if (!_enabled)
+            return;
+        _invisibleLeft = [];
+        _invisibleRight = [];
+        const menuWidth = _menu.clientWidth;
+        if (_menu.scrollWidth > menuWidth || _marginLeft < 0) {
+            Array.from(_menu.children).forEach((child) => {
+                const offsetLeft = child.offsetLeft;
+                if (offsetLeft < 0) {
+                    _invisibleLeft.push(child);
                 }
-                _buttonShowNext.classList.add('active');
-            }
-        },
-        /**
-         * Sets the first item's margin-left value that is
-         * used to move the menu contents around.
-         *
-         * @param       {int}   offset  changes to the margin-left value in pixel
-         * @protected
-         */
-        _setMarginLeft: function (offset) {
-            _marginLeft = Math.min(_marginLeft + offset, 0);
-            _firstElement.style.setProperty('margin-left', _marginLeft + 'px', '');
-        },
-        /**
-         * Toggles button overlays and rebuilds the list
-         * of invisible items from left to right.
-         *
-         * @protected
-         */
-        _rebuildVisibility: function () {
-            if (!_enabled)
-                return;
-            _invisibleLeft = [];
-            _invisibleRight = [];
-            var menuWidth = _menu.clientWidth;
-            if (_menu.scrollWidth > menuWidth || _marginLeft < 0) {
-                var child;
-                for (var i = 0, length = _menu.childElementCount; i < length; i++) {
-                    child = _menu.children[i];
-                    var offsetLeft = child.offsetLeft;
-                    if (offsetLeft < 0) {
-                        _invisibleLeft.push(child);
-                    }
-                    else if (offsetLeft + child.clientWidth > menuWidth) {
-                        _invisibleRight.push(child);
-                    }
+                else if (offsetLeft + child.clientWidth > menuWidth) {
+                    _invisibleRight.push(child);
                 }
-            }
-            _buttonShowPrevious.classList[(_invisibleLeft.length ? 'add' : 'remove')]('active');
-            _buttonShowNext.classList[(_invisibleRight.length ? 'add' : 'remove')]('active');
-        },
-        /**
-         * Builds the UI and binds the event listeners.
-         *
-         * @protected
-         */
-        _setup: function () {
-            this._setupOverflow();
-            this._setupA11y();
-        },
-        /**
-         * Setups overflow handling.
-         *
-         * @protected
-         */
-        _setupOverflow: function () {
-            _buttonShowNext = elCreate('a');
-            _buttonShowNext.className = 'mainMenuShowNext';
-            _buttonShowNext.href = '#';
-            _buttonShowNext.innerHTML = '<span class="icon icon32 fa-angle-right"></span>';
-            elAttr(_buttonShowNext, 'aria-hidden', 'true');
-            _buttonShowNext.addEventListener(WCF_CLICK_EVENT, this._showNext.bind(this));
-            _menu.parentNode.appendChild(_buttonShowNext);
-            _buttonShowPrevious = elCreate('a');
-            _buttonShowPrevious.className = 'mainMenuShowPrevious';
-            _buttonShowPrevious.href = '#';
-            _buttonShowPrevious.innerHTML = '<span class="icon icon32 fa-angle-left"></span>';
-            elAttr(_buttonShowPrevious, 'aria-hidden', 'true');
-            _buttonShowPrevious.addEventListener(WCF_CLICK_EVENT, this._showPrevious.bind(this));
-            _menu.parentNode.insertBefore(_buttonShowPrevious, _menu.parentNode.firstChild);
-            var rebuildVisibility = this._rebuildVisibility.bind(this);
-            _firstElement.addEventListener('transitionend', rebuildVisibility);
-            window.addEventListener('resize', function () {
-                _firstElement.style.setProperty('margin-left', '0px', '');
-                _marginLeft = 0;
-                rebuildVisibility();
             });
-            this._enable();
-        },
-        /**
-         * Setups a11y improvements.
-         *
-         * @protected
-         */
-        _setupA11y: function () {
-            elBySelAll('.boxMenuHasChildren', _menu, (function (element) {
-                var showMenu = false;
-                var link = elBySel('.boxMenuLink', element);
-                if (link) {
-                    elAttr(link, 'aria-haspopup', true);
-                    elAttr(link, 'aria-expanded', showMenu);
-                }
-                var showMenuButton = elCreate('button');
-                showMenuButton.className = 'visuallyHidden';
-                showMenuButton.tabindex = 0;
-                elAttr(showMenuButton, 'role', 'button');
-                elAttr(showMenuButton, 'aria-label', Language.get('wcf.global.button.showMenu'));
-                element.insertBefore(showMenuButton, link.nextSibling);
-                showMenuButton.addEventListener(WCF_CLICK_EVENT, function () {
-                    showMenu = !showMenu;
-                    elAttr(link, 'aria-expanded', showMenu);
-                    elAttr(showMenuButton, 'aria-label', (showMenu ? Language.get('wcf.global.button.hideMenu') : Language.get('wcf.global.button.showMenu')));
-                });
-            }).bind(this));
         }
-    };
+        _buttonShowPrevious.classList[(_invisibleLeft.length ? 'add' : 'remove')]('active');
+        _buttonShowNext.classList[(_invisibleRight.length ? 'add' : 'remove')]('active');
+    }
+    /**
+     * Builds the UI and binds the event listeners.
+     */
+    function setup() {
+        setupOverflow();
+        setupA11y();
+    }
+    /**
+     * Setups overflow handling.
+     */
+    function setupOverflow() {
+        const menuParent = _menu.parentElement;
+        _buttonShowNext = document.createElement('a');
+        _buttonShowNext.className = 'mainMenuShowNext';
+        _buttonShowNext.href = '#';
+        _buttonShowNext.innerHTML = '<span class="icon icon32 fa-angle-right"></span>';
+        _buttonShowNext.setAttribute('aria-hidden', 'true');
+        _buttonShowNext.addEventListener('click', showNext);
+        menuParent.appendChild(_buttonShowNext);
+        _buttonShowPrevious = document.createElement('a');
+        _buttonShowPrevious.className = 'mainMenuShowPrevious';
+        _buttonShowPrevious.href = '#';
+        _buttonShowPrevious.innerHTML = '<span class="icon icon32 fa-angle-left"></span>';
+        _buttonShowPrevious.setAttribute('aria-hidden', 'true');
+        _buttonShowPrevious.addEventListener('click', showPrevious);
+        menuParent.insertBefore(_buttonShowPrevious, menuParent.firstChild);
+        _firstElement.addEventListener('transitionend', rebuildVisibility);
+        window.addEventListener('resize', () => {
+            _firstElement.style.setProperty('margin-left', '0px', '');
+            _marginLeft = 0;
+            rebuildVisibility();
+        });
+        enable();
+    }
+    /**
+     * Setups a11y improvements.
+     */
+    function setupA11y() {
+        _menu.querySelectorAll('.boxMenuHasChildren').forEach(element => {
+            const link = element.querySelector('.boxMenuLink');
+            link.setAttribute('aria-haspopup', 'true');
+            link.setAttribute('aria-expanded', 'false');
+            const showMenuButton = document.createElement('button');
+            showMenuButton.className = 'visuallyHidden';
+            showMenuButton.tabIndex = 0;
+            showMenuButton.setAttribute('role', 'button');
+            showMenuButton.setAttribute('aria-label', Language.get('wcf.global.button.showMenu'));
+            element.insertBefore(showMenuButton, link.nextSibling);
+            let showMenu = false;
+            showMenuButton.addEventListener('click', () => {
+                showMenu = !showMenu;
+                link.setAttribute('aria-expanded', showMenu ? 'true' : 'false');
+                showMenuButton.setAttribute('aria-label', Language.get(showMenu ? 'wcf.global.button.hideMenu' : 'wcf.global.button.showMenu'));
+            });
+        });
+    }
+    /**
+     * Initializes the main menu overflow handling.
+     */
+    function init() {
+        const menu = document.querySelector('.mainMenu .boxMenu');
+        const firstElement = (menu && menu.childElementCount) ? menu.children[0] : null;
+        if (firstElement === null) {
+            throw new Error("Unable to find the main menu.");
+        }
+        _menu = menu;
+        _firstElement = firstElement;
+        UiScreen.on('screen-lg', {
+            match: enable,
+            unmatch: disable,
+            setup: setup,
+        });
+    }
+    exports.init = init;
 });
index 62cb971ec4133974dc704dcfdab0617056e8d18e..3acd266caf83daa6abdad4e60036cde8d872b06a 100644 (file)
@@ -25,10 +25,8 @@ let _userPanelSearchButton: HTMLElement;
 
 /**
  * Provides the collapsible search bar.
- *
- * @protected
  */
-function initSearchBar() {
+function initSearchBar(): void {
   _pageHeaderSearch = document.getElementById('pageHeaderSearch')!;
   _pageHeaderSearch.addEventListener('click', ev => ev.stopPropagation());
 
@@ -67,10 +65,8 @@ function initSearchBar() {
 
 /**
  * Opens the search bar.
- *
- * @protected
  */
-function openSearchBar() {
+function openSearchBar(): void {
   window.WCF.Dropdown.Interactive.Handler.closeAll();
 
   _pageHeader.classList.add('searchBarOpen');
@@ -93,10 +89,8 @@ function openSearchBar() {
 
 /**
  * Closes the search bar.
- *
- * @protected
  */
-function closeSearchBar() {
+function closeSearchBar(): void {
   _pageHeader.classList.remove('searchBarOpen');
   _userPanelSearchButton.parentElement!.classList.remove('open');
 
@@ -114,7 +108,7 @@ function closeSearchBar() {
 /**
  * Initializes the sticky page header handler.
  */
-export function init() {
+export function init(): void {
   _pageHeader = document.getElementById('pageHeader')!;
   _pageHeaderContainer = document.getElementById('pageHeaderContainer')!;
 
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Page/Header/Menu.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Page/Header/Menu.js
deleted file mode 100644 (file)
index 4b56f1d..0000000
+++ /dev/null
@@ -1,239 +0,0 @@
-/**
- * Handles main menu overflow and a11y.
- * 
- * @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/Page/Header/Menu
- */
-define(['Environment', 'Language', 'Ui/Screen'], function(Environment, Language, UiScreen) {
-       "use strict";
-       
-       var _enabled = false;
-       
-       // elements
-       var _buttonShowNext, _buttonShowPrevious, _firstElement, _menu;
-       
-       // internal states
-       var _marginLeft = 0, _invisibleLeft = [], _invisibleRight = [];
-       
-       /**
-        * @exports     WoltLabSuite/Core/Ui/Page/Header/Menu
-        */
-       return {
-               /**
-                * Initializes the main menu overflow handling.
-                */
-               init: function () {
-                       _menu = elBySel('.mainMenu .boxMenu');
-                       _firstElement = (_menu && _menu.childElementCount) ? _menu.children[0] : null;
-                       if (_firstElement === null) {
-                               throw new Error("Unable to find the menu.");
-                       }
-                       
-                       UiScreen.on('screen-lg', {
-                               enable: this._enable.bind(this),
-                               disable: this._disable.bind(this),
-                               setup: this._setup.bind(this)
-                       });
-               },
-               
-               /**
-                * Enables the overflow handler.
-                * 
-                * @protected
-                */
-               _enable: function () {
-                       _enabled = true;
-                       
-                       // Safari waits three seconds for a font to be loaded which causes the header menu items
-                       // to be extremely wide while waiting for the font to be loaded. The extremely wide menu
-                       // items in turn can cause the overflow controls to be shown even if the width of the header
-                       // menu, after the font has been loaded successfully, does not require them. This width
-                       // issue results in the next button being shown for a short time. To circumvent this issue,
-                       // we wait a second before showing the obverflow controls in Safari.
-                       // see https://webkit.org/blog/6643/improved-font-loading/
-                       if (Environment.browser() === 'safari') {
-                               window.setTimeout(this._rebuildVisibility.bind(this), 1000);
-                       }
-                       else {
-                               this._rebuildVisibility();
-                               
-                               // IE11 sometimes suffers from a timing issue
-                               window.setTimeout(this._rebuildVisibility.bind(this), 1000);
-                       }
-               },
-               
-               /**
-                * Disables the overflow handler.
-                * 
-                * @protected
-                */
-               _disable: function () {
-                       _enabled = false;
-               },
-               
-               /**
-                * Displays the next three menu items.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _showNext: function(event) {
-                       event.preventDefault();
-                       
-                       if (_invisibleRight.length) {
-                               var showItem = _invisibleRight.slice(0, 3).pop();
-                               this._setMarginLeft(_menu.clientWidth - (showItem.offsetLeft + showItem.clientWidth));
-                               
-                               if (_menu.lastElementChild === showItem) {
-                                       _buttonShowNext.classList.remove('active');
-                               }
-                               
-                               _buttonShowPrevious.classList.add('active');
-                       }
-               },
-               
-               /**
-                * Displays the previous three menu items.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _showPrevious: function (event) {
-                       event.preventDefault();
-                       
-                       if (_invisibleLeft.length) {
-                               var showItem = _invisibleLeft.slice(-3)[0];
-                               this._setMarginLeft(showItem.offsetLeft * -1);
-                               
-                               if (_menu.firstElementChild === showItem) {
-                                       _buttonShowPrevious.classList.remove('active');
-                               }
-                               
-                               _buttonShowNext.classList.add('active');
-                       }
-               },
-               
-               /**
-                * Sets the first item's margin-left value that is
-                * used to move the menu contents around.
-                * 
-                * @param       {int}   offset  changes to the margin-left value in pixel
-                * @protected
-                */
-               _setMarginLeft: function (offset) {
-                       _marginLeft = Math.min(_marginLeft + offset, 0);
-                       
-                       _firstElement.style.setProperty('margin-left', _marginLeft + 'px', '');
-               },
-               
-               /**
-                * Toggles button overlays and rebuilds the list
-                * of invisible items from left to right.
-                * 
-                * @protected
-                */
-               _rebuildVisibility: function () {
-                       if (!_enabled) return;
-                       
-                       _invisibleLeft = [];
-                       _invisibleRight = [];
-                       
-                       var menuWidth = _menu.clientWidth;
-                       if (_menu.scrollWidth > menuWidth || _marginLeft < 0) {
-                               var child;
-                               for (var i = 0, length = _menu.childElementCount; i < length; i++) {
-                                       child = _menu.children[i];
-                                       
-                                       var offsetLeft = child.offsetLeft;
-                                       if (offsetLeft < 0) {
-                                               _invisibleLeft.push(child);
-                                       }
-                                       else if (offsetLeft + child.clientWidth > menuWidth) {
-                                               _invisibleRight.push(child);
-                                       }
-                               }
-                       }
-                       
-                       _buttonShowPrevious.classList[(_invisibleLeft.length ? 'add' : 'remove')]('active');
-                       _buttonShowNext.classList[(_invisibleRight.length ? 'add' : 'remove')]('active');
-               },
-               
-               /**
-                * Builds the UI and binds the event listeners.
-                *
-                * @protected
-                */
-               _setup: function () {
-                       this._setupOverflow();
-                       this._setupA11y();
-               },
-               
-               /**
-                * Setups overflow handling.
-                * 
-                * @protected
-                */
-               _setupOverflow: function () {
-                       _buttonShowNext = elCreate('a');
-                       _buttonShowNext.className = 'mainMenuShowNext';
-                       _buttonShowNext.href = '#';
-                       _buttonShowNext.innerHTML = '<span class="icon icon32 fa-angle-right"></span>';
-                       elAttr(_buttonShowNext, 'aria-hidden', 'true');
-                       _buttonShowNext.addEventListener(WCF_CLICK_EVENT, this._showNext.bind(this));
-                       
-                       _menu.parentNode.appendChild(_buttonShowNext);
-                       
-                       _buttonShowPrevious = elCreate('a');
-                       _buttonShowPrevious.className = 'mainMenuShowPrevious';
-                       _buttonShowPrevious.href = '#';
-                       _buttonShowPrevious.innerHTML = '<span class="icon icon32 fa-angle-left"></span>';
-                       elAttr(_buttonShowPrevious, 'aria-hidden', 'true');
-                       _buttonShowPrevious.addEventListener(WCF_CLICK_EVENT, this._showPrevious.bind(this));
-                       
-                       _menu.parentNode.insertBefore(_buttonShowPrevious, _menu.parentNode.firstChild);
-                       
-                       var rebuildVisibility = this._rebuildVisibility.bind(this);
-                       _firstElement.addEventListener('transitionend', rebuildVisibility);
-                       
-                       window.addEventListener('resize', function () {
-                               _firstElement.style.setProperty('margin-left', '0px', '');
-                               _marginLeft = 0;
-                               
-                               rebuildVisibility();
-                       });
-                       
-                       this._enable();
-               },
-               
-               /**
-                * Setups a11y improvements.
-                *
-                * @protected
-                */
-               _setupA11y: function() {
-                       elBySelAll('.boxMenuHasChildren', _menu, (function(element) {
-                               var showMenu = false;
-                               var link = elBySel('.boxMenuLink', element);
-                               if (link) {
-                                       elAttr(link, 'aria-haspopup', true);
-                                       elAttr(link, 'aria-expanded', showMenu);
-                               }
-                               
-                               var showMenuButton = elCreate('button');
-                               showMenuButton.className = 'visuallyHidden';
-                               showMenuButton.tabindex = 0;
-                               elAttr(showMenuButton, 'role', 'button');
-                               elAttr(showMenuButton, 'aria-label', Language.get('wcf.global.button.showMenu'));
-                               element.insertBefore(showMenuButton, link.nextSibling);
-                               
-                               showMenuButton.addEventListener(WCF_CLICK_EVENT, function() {
-                                       showMenu = !showMenu;
-                                       elAttr(link, 'aria-expanded', showMenu);
-                                       elAttr(showMenuButton, 'aria-label', (showMenu ? Language.get('wcf.global.button.hideMenu') : Language.get('wcf.global.button.showMenu')));
-                               });
-                       }).bind(this));
-               }
-       };
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Page/Header/Menu.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Page/Header/Menu.ts
new file mode 100644 (file)
index 0000000..6e967f1
--- /dev/null
@@ -0,0 +1,214 @@
+/**
+ * Handles main menu overflow and a11y.
+ *
+ * @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/Page/Header/Menu
+ */
+
+import * as Environment from '../../../Environment';
+import * as Language from '../../../Language';
+import * as UiScreen from '../../Screen';
+
+let _enabled = false;
+
+let _buttonShowNext: HTMLAnchorElement;
+let _buttonShowPrevious: HTMLAnchorElement;
+let _firstElement: HTMLElement;
+let _menu: HTMLElement;
+
+let _marginLeft = 0;
+let _invisibleLeft: HTMLElement[] = [];
+let _invisibleRight: HTMLElement[] = [];
+
+/**
+ * Enables the overflow handler.
+ */
+function enable(): void {
+  _enabled = true;
+
+  // Safari waits three seconds for a font to be loaded which causes the header menu items
+  // to be extremely wide while waiting for the font to be loaded. The extremely wide menu
+  // items in turn can cause the overflow controls to be shown even if the width of the header
+  // menu, after the font has been loaded successfully, does not require them. This width
+  // issue results in the next button being shown for a short time. To circumvent this issue,
+  // we wait a second before showing the obverflow controls in Safari.
+  // see https://webkit.org/blog/6643/improved-font-loading/
+  if (Environment.browser() === 'safari') {
+    window.setTimeout(rebuildVisibility, 1000);
+  } else {
+    rebuildVisibility();
+
+    // IE11 sometimes suffers from a timing issue
+    window.setTimeout(rebuildVisibility, 1000);
+  }
+}
+
+/**
+ * Disables the overflow handler.
+ */
+function disable(): void {
+  _enabled = false;
+}
+
+/**
+ * Displays the next three menu items.
+ */
+function showNext(event: MouseEvent): void {
+  event.preventDefault();
+
+  if (_invisibleRight.length) {
+    const showItem = _invisibleRight.slice(0, 3).pop()!;
+    setMarginLeft(_menu.clientWidth - (showItem.offsetLeft + showItem.clientWidth));
+
+    if (_menu.lastElementChild === showItem) {
+      _buttonShowNext.classList.remove('active');
+    }
+
+    _buttonShowPrevious.classList.add('active');
+  }
+}
+
+/**
+ * Displays the previous three menu items.
+ */
+function showPrevious(event: MouseEvent): void {
+  event.preventDefault();
+
+  if (_invisibleLeft.length) {
+    const showItem = _invisibleLeft.slice(-3)[0];
+    setMarginLeft(showItem.offsetLeft * -1);
+
+    if (_menu.firstElementChild === showItem) {
+      _buttonShowPrevious.classList.remove('active');
+    }
+
+    _buttonShowNext.classList.add('active');
+  }
+}
+
+/**
+ * Sets the first item's margin-left value that is
+ * used to move the menu contents around.
+ */
+function setMarginLeft(offset: number): void {
+  _marginLeft = Math.min(_marginLeft + offset, 0);
+
+  _firstElement.style.setProperty('margin-left', _marginLeft + 'px', '');
+}
+
+/**
+ * Toggles button overlays and rebuilds the list
+ * of invisible items from left to right.
+ */
+function rebuildVisibility(): void {
+  if (!_enabled) return;
+
+  _invisibleLeft = [];
+  _invisibleRight = [];
+
+  const menuWidth = _menu.clientWidth;
+  if (_menu.scrollWidth > menuWidth || _marginLeft < 0) {
+    Array.from(_menu.children).forEach((child: HTMLElement) => {
+      const offsetLeft = child.offsetLeft;
+      if (offsetLeft < 0) {
+        _invisibleLeft.push(child);
+      } else if (offsetLeft + child.clientWidth > menuWidth) {
+        _invisibleRight.push(child);
+      }
+    });
+  }
+
+  _buttonShowPrevious.classList[(_invisibleLeft.length ? 'add' : 'remove')]('active');
+  _buttonShowNext.classList[(_invisibleRight.length ? 'add' : 'remove')]('active');
+}
+
+/**
+ * Builds the UI and binds the event listeners.
+ */
+function setup(): void {
+  setupOverflow();
+  setupA11y();
+}
+
+/**
+ * Setups overflow handling.
+ */
+function setupOverflow(): void {
+  const menuParent = _menu.parentElement!;
+
+  _buttonShowNext = document.createElement('a');
+  _buttonShowNext.className = 'mainMenuShowNext';
+  _buttonShowNext.href = '#';
+  _buttonShowNext.innerHTML = '<span class="icon icon32 fa-angle-right"></span>';
+  _buttonShowNext.setAttribute('aria-hidden', 'true');
+  _buttonShowNext.addEventListener('click', showNext);
+
+  menuParent.appendChild(_buttonShowNext);
+
+  _buttonShowPrevious = document.createElement('a');
+  _buttonShowPrevious.className = 'mainMenuShowPrevious';
+  _buttonShowPrevious.href = '#';
+  _buttonShowPrevious.innerHTML = '<span class="icon icon32 fa-angle-left"></span>';
+  _buttonShowPrevious.setAttribute('aria-hidden', 'true');
+  _buttonShowPrevious.addEventListener('click', showPrevious);
+
+  menuParent.insertBefore(_buttonShowPrevious, menuParent.firstChild);
+
+  _firstElement.addEventListener('transitionend', rebuildVisibility);
+
+  window.addEventListener('resize', () => {
+    _firstElement.style.setProperty('margin-left', '0px', '');
+    _marginLeft = 0;
+
+    rebuildVisibility();
+  });
+
+  enable();
+}
+
+/**
+ * Setups a11y improvements.
+ */
+function setupA11y(): void {
+  _menu.querySelectorAll('.boxMenuHasChildren').forEach(element => {
+    const link = element.querySelector('.boxMenuLink')!;
+    link.setAttribute('aria-haspopup', 'true');
+    link.setAttribute('aria-expanded', 'false');
+
+    const showMenuButton = document.createElement('button');
+    showMenuButton.className = 'visuallyHidden';
+    showMenuButton.tabIndex = 0;
+    showMenuButton.setAttribute('role', 'button');
+    showMenuButton.setAttribute('aria-label', Language.get('wcf.global.button.showMenu'));
+    element.insertBefore(showMenuButton, link.nextSibling);
+
+    let showMenu = false;
+    showMenuButton.addEventListener('click', () => {
+      showMenu = !showMenu;
+      link.setAttribute('aria-expanded', showMenu ? 'true' : 'false');
+      showMenuButton.setAttribute('aria-label', Language.get(showMenu ? 'wcf.global.button.hideMenu' : 'wcf.global.button.showMenu'));
+    });
+  });
+}
+
+/**
+ * Initializes the main menu overflow handling.
+ */
+export function init(): void {
+  const menu = document.querySelector('.mainMenu .boxMenu') as HTMLElement;
+  const firstElement = (menu && menu.childElementCount) ? menu.children[0] as HTMLElement : null;
+  if (firstElement === null) {
+    throw new Error("Unable to find the main menu.");
+  }
+
+  _menu = menu;
+  _firstElement = firstElement;
+
+  UiScreen.on('screen-lg', {
+    match: enable,
+    unmatch: disable,
+    setup: setup,
+  });
+}