From 9ada5b424af04c8165c77121c3bebc6535be7420 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Fri, 6 Nov 2020 23:03:40 +0100 Subject: [PATCH] Convert `Ui/Mobile` to TypeScript --- .../files/js/WoltLabSuite/Core/Bootstrap.js | 4 +- .../files/js/WoltLabSuite/Core/Ui/Mobile.js | 711 +++++++++--------- .../files/ts/WoltLabSuite/Core/Bootstrap.js | 4 +- .../files/ts/WoltLabSuite/Core/Ui/Mobile.js | 377 ---------- .../files/ts/WoltLabSuite/Core/Ui/Mobile.ts | 446 +++++++++++ 5 files changed, 810 insertions(+), 732 deletions(-) delete mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Mobile.js create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Mobile.ts diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js index d9b4029283..d8510dff09 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js @@ -67,9 +67,7 @@ define([ DateTimeRelative.setup(); DatePicker.init(); UiSimpleDropdown.setup(); - UiMobile.setup({ - enableMobileMenu: options.enableMobileMenu - }); + UiMobile.setup(options.enableMobileMenu); UiTabMenu.setup(); //UiFlexibleMenu.setup(); UiDialog.setup(); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Mobile.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Mobile.js index 11398a3270..219ec1b0c7 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Mobile.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Mobile.js @@ -1,377 +1,390 @@ /** * Modifies the interface to provide a better usability for mobile devices. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Ui/Mobile + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/Mobile */ -define(['Core', 'Environment', 'EventHandler', 'Language', 'List', 'Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util', 'Ui/Alignment', 'Ui/CloseOverlay', 'Ui/Screen', './Page/Menu/Main', './Page/Menu/User', 'WoltLabSuite/Core/Ui/Dropdown/Reusable'], function (Core, Environment, EventHandler, Language, List, DomChangeListener, DomTraverse, DomUtil, UiAlignment, UiCloseOverlay, UiScreen, UiPageMenuMain, UiPageMenuUser, UiDropdownReusable) { +define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../Environment", "../Event/Handler", "./Alignment", "./CloseOverlay", "./Dropdown/Reusable", "./Page/Menu/Main", "./Page/Menu/User", "./Screen"], function (require, exports, tslib_1, Core, Listener_1, Environment, EventHandler, UiAlignment, CloseOverlay_1, UiDropdownReusable, Main_1, User_1, UiScreen) { "use strict"; - var _buttonGroupNavigations = elByClass('buttonGroupNavigation'); - var _callbackCloseDropdown = null; - var _dropdownMenu = null; - var _dropdownMenuMessage = null; - var _enabled = false; - var _enabledLGTouchNavigation = false; - var _knownMessages = new List(); - var _main = null; - var _messages = elByClass('message'); - var _mobileSidebarEnabled = false; - var _options = {}; - var _pageMenuMain = null; - var _pageMenuUser = null; - var _messageGroups = null; - var _sidebars = []; - /** - * @exports WoltLabSuite/Core/Ui/Mobile - */ - return { - /** - * Initializes the mobile UI. - * - * @param {Object=} options initialization options - */ - setup: function (options) { - _options = Core.extend({ - enableMobileMenu: true - }, options); - _main = elById('main'); - elBySelAll('.sidebar', undefined, function (sidebar) { - _sidebars.push(sidebar); - }); - if (Environment.touch()) { - document.documentElement.classList.add('touch'); + Object.defineProperty(exports, "__esModule", { value: true }); + exports.removeShadow = exports.rebuildShadow = exports.disableShadow = exports.disable = exports.enableShadow = exports.enable = exports.setup = void 0; + Core = tslib_1.__importStar(Core); + Listener_1 = tslib_1.__importDefault(Listener_1); + Environment = tslib_1.__importStar(Environment); + EventHandler = tslib_1.__importStar(EventHandler); + UiAlignment = tslib_1.__importStar(UiAlignment); + CloseOverlay_1 = tslib_1.__importDefault(CloseOverlay_1); + UiDropdownReusable = tslib_1.__importStar(UiDropdownReusable); + Main_1 = tslib_1.__importDefault(Main_1); + User_1 = tslib_1.__importDefault(User_1); + UiScreen = tslib_1.__importStar(UiScreen); + let _dropdownMenu = null; + let _dropdownMenuMessage = null; + let _enabled = false; + let _enabledLGTouchNavigation = false; + let _enableMobileMenu = false; + const _knownMessages = new WeakSet(); + let _mobileSidebarEnabled = false; + let _pageMenuMain; + let _pageMenuUser; + let _messageGroups = null; + const _sidebars = []; + function _init() { + _enabled = true; + initSearchBar(); + _initButtonGroupNavigation(); + _initMessages(); + _initMobileMenu(); + CloseOverlay_1.default.add("WoltLabSuite/Core/Ui/Mobile", _closeAllMenus); + Listener_1.default.add("WoltLabSuite/Core/Ui/Mobile", () => { + _initButtonGroupNavigation(); + _initMessages(); + }); + } + function initSearchBar() { + const searchBar = document.getElementById("pageHeaderSearch"); + const searchInput = document.getElementById("pageHeaderSearchInput"); + let scrollTop = null; + EventHandler.add("com.woltlab.wcf.MainMenuMobile", "more", (data) => { + if (data.identifier === "com.woltlab.wcf.search") { + data.handler.close(); + if (Environment.platform() === "ios") { + scrollTop = document.body.scrollTop; + UiScreen.scrollDisable(); + } + const pageHeader = document.getElementById("pageHeader"); + searchBar.style.setProperty("top", `${pageHeader.offsetHeight}px`, ""); + searchBar.classList.add("open"); + searchInput.focus(); + if (Environment.platform() === "ios") { + document.body.scrollTop = 0; + } } - if (Environment.platform() !== 'desktop') { - document.documentElement.classList.add('mobile'); + }); + document.getElementById("main").addEventListener("click", () => { + if (searchBar) { + searchBar.classList.remove("open"); } - var messageGroupList = elBySel('.messageGroupList'); - if (messageGroupList) - _messageGroups = elByClass('messageGroup', messageGroupList); - UiScreen.on('screen-md-down', { - match: this.enable.bind(this), - unmatch: this.disable.bind(this), - setup: this._init.bind(this) - }); - UiScreen.on('screen-sm-down', { - match: this.enableShadow.bind(this), - unmatch: this.disableShadow.bind(this), - setup: this.enableShadow.bind(this) - }); - UiScreen.on('screen-md-down', { - match: this._enableMobileSidebar.bind(this), - unmatch: this._disableMobileSidebar.bind(this), - setup: this._setupMobileSidebar.bind(this) - }); - // On the large tablets (e.g. iPad Pro) the navigation is not usable, because there is not the mobile - // layout displayed, but the normal one for the desktop. The navigation reacts to a hover status if a - // menu item has several submenu items. Logically, this cannot be created with the tablet, so that we - // display the submenu here after a single click and only follow the link after another click. - if (Environment.touch() && (Environment.platform() === 'ios' || Environment.platform() === 'android')) { - UiScreen.on('screen-lg', { - match: this._enableLGTouchNavigation.bind(this), - unmatch: this._disableLGTouchNavigation.bind(this), - setup: this._setupLGTouchNavigation.bind(this) - }); + if (Environment.platform() === "ios" && scrollTop) { + UiScreen.scrollEnable(); + document.body.scrollTop = scrollTop; + scrollTop = null; } - }, - /** - * Enables the mobile UI. - */ - enable: function () { - _enabled = true; - if (_options.enableMobileMenu) { - _pageMenuMain.enable(); - _pageMenuUser.enable(); + }); + } + function _initButtonGroupNavigation() { + document.querySelectorAll(".buttonGroupNavigation").forEach((navigation) => { + if (navigation.classList.contains("jsMobileButtonGroupNavigation")) { + return; } - }, - /** - * Enables shadow links for larger click areas on messages. - */ - enableShadow: function () { - if (_messageGroups) - this.rebuildShadow(_messageGroups, '.messageGroupLink'); - }, - /** - * Disables the mobile UI. - */ - disable: function () { - _enabled = false; - if (_options.enableMobileMenu) { - _pageMenuMain.disable(); - _pageMenuUser.disable(); + else { + navigation.classList.add("jsMobileButtonGroupNavigation"); } - }, - /** - * Disables shadow links. - */ - disableShadow: function () { - if (_messageGroups) - this.removeShadow(_messageGroups); - if (_dropdownMenu) - _callbackCloseDropdown(); - }, - _init: function () { - _enabled = true; - this._initSearchBar(); - this._initButtonGroupNavigation(); - this._initMessages(); - this._initMobileMenu(); - UiCloseOverlay.add('WoltLabSuite/Core/Ui/Mobile', this._closeAllMenus.bind(this)); - DomChangeListener.add('WoltLabSuite/Core/Ui/Mobile', (function () { - this._initButtonGroupNavigation(); - this._initMessages(); - }).bind(this)); - }, - _initSearchBar: function () { - var _searchBar = elById('pageHeaderSearch'); - var _searchInput = elById('pageHeaderSearchInput'); - var scrollTop = null; - EventHandler.add('com.woltlab.wcf.MainMenuMobile', 'more', function (data) { - if (data.identifier === 'com.woltlab.wcf.search') { - data.handler.close(true); - if (Environment.platform() === 'ios') { - scrollTop = document.body.scrollTop; - UiScreen.scrollDisable(); - } - _searchBar.style.setProperty('top', elById('pageHeader').offsetHeight + 'px', ''); - _searchBar.classList.add('open'); - _searchInput.focus(); - if (Environment.platform() === 'ios') { - document.body.scrollTop = 0; - } - } + const list = navigation.querySelector(".buttonList"); + if (list.childElementCount === 0) { + // ignore objects without options + return; + } + navigation.parentElement.classList.add("hasMobileNavigation"); + const button = document.createElement("a"); + button.className = "dropdownLabel"; + const span = document.createElement("span"); + span.className = "icon icon24 fa-ellipsis-v"; + button.appendChild(span); + button.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + navigation.classList.toggle("open"); }); - _main.addEventListener('click', function () { - if (_searchBar) - _searchBar.classList.remove('open'); - if (Environment.platform() === 'ios' && scrollTop !== null) { - UiScreen.scrollEnable(); - document.body.scrollTop = scrollTop; - scrollTop = null; - } + list.addEventListener("click", function (event) { + event.stopPropagation(); + navigation.classList.remove("open"); }); - }, - _initButtonGroupNavigation: function () { - for (var i = 0, length = _buttonGroupNavigations.length; i < length; i++) { - var navigation = _buttonGroupNavigations[i]; - if (navigation.classList.contains('jsMobileButtonGroupNavigation')) - continue; - else - navigation.classList.add('jsMobileButtonGroupNavigation'); - var list = elBySel('.buttonList', navigation); - if (list.childElementCount === 0) { - // ignore objects without options - continue; - } - navigation.parentNode.classList.add('hasMobileNavigation'); - var button = elCreate('a'); - button.className = 'dropdownLabel'; - var span = elCreate('span'); - span.className = 'icon icon24 fa-ellipsis-v'; - button.appendChild(span); - (function (navigation, button, list) { - button.addEventListener('click', function (event) { - event.preventDefault(); - event.stopPropagation(); - navigation.classList.toggle('open'); - }); - list.addEventListener('click', function (event) { - event.stopPropagation(); - navigation.classList.remove('open'); - }); - })(navigation, button, list); - navigation.insertBefore(button, navigation.firstChild); + navigation.insertBefore(button, navigation.firstChild); + }); + } + function _initMessages() { + document.querySelectorAll(".message").forEach((message) => { + if (_knownMessages.has(message)) { + return; } - }, - _initMessages: function () { - Array.prototype.forEach.call(_messages, (function (message) { - if (_knownMessages.has(message)) { - return; - } - var navigation = elBySel('.jsMobileNavigation', message); - if (navigation) { - navigation.addEventListener('click', function (event) { - event.stopPropagation(); - // mimic dropdown behavior - window.setTimeout(function () { - navigation.classList.remove('open'); - }, 10); + const navigation = message.querySelector(".jsMobileNavigation"); + if (navigation) { + navigation.addEventListener("click", (event) => { + event.stopPropagation(); + // mimic dropdown behavior + window.setTimeout(() => { + navigation.classList.remove("open"); + }, 10); + }); + const quickOptions = message.querySelector(".messageQuickOptions"); + if (quickOptions && navigation.childElementCount) { + quickOptions.classList.add("active"); + quickOptions.addEventListener("click", (event) => { + const target = event.target; + if (_enabled && UiScreen.is("screen-sm-down") && target.nodeName !== "LABEL" && target.nodeName !== "INPUT") { + event.preventDefault(); + event.stopPropagation(); + _toggleMobileNavigation(message, quickOptions, navigation); + } }); - var quickOptions = elBySel('.messageQuickOptions', message); - if (quickOptions && navigation.childElementCount) { - quickOptions.classList.add('active'); - quickOptions.addEventListener('click', (function (event) { - if (_enabled && UiScreen.is('screen-sm-down') && event.target.nodeName !== 'LABEL' && event.target.nodeName !== 'INPUT') { - event.preventDefault(); - event.stopPropagation(); - this._toggleMobileNavigation(message, quickOptions, navigation); - } - }).bind(this)); - } } - _knownMessages.add(message); - }).bind(this)); - }, - _initMobileMenu: function () { - if (_options.enableMobileMenu) { - _pageMenuMain = new UiPageMenuMain(); - _pageMenuUser = new UiPageMenuUser(); } - }, - _closeAllMenus: function () { - elBySelAll('.jsMobileButtonGroupNavigation.open, .jsMobileNavigation.open', null, function (menu) { - menu.classList.remove('open'); - }); - if (_enabled && _dropdownMenu) - _callbackCloseDropdown(); - }, - rebuildShadow: function (elements, linkSelector) { - var element, parent, shadow; - for (var i = 0, length = elements.length; i < length; i++) { - element = elements[i]; - parent = element.parentNode; - shadow = DomTraverse.childByClass(parent, 'mobileLinkShadow'); - if (shadow === null) { - if (elBySel(linkSelector, element).href) { - shadow = elCreate('a'); - shadow.className = 'mobileLinkShadow'; - shadow.href = elBySel(linkSelector, element).href; - parent.appendChild(shadow); - parent.classList.add('mobileLinkShadowContainer'); - } - } - } - }, - removeShadow: function (elements) { - var element, parent, shadow; - for (var i = 0, length = elements.length; i < length; i++) { - element = elements[i]; - parent = element.parentNode; - if (parent.classList.contains('mobileLinkShadowContainer')) { - shadow = DomTraverse.childByClass(parent, 'mobileLinkShadow'); - if (shadow !== null) { - elRemove(shadow); - } - parent.classList.remove('mobileLinkShadowContainer'); + _knownMessages.add(message); + }); + } + function _initMobileMenu() { + if (_enableMobileMenu) { + _pageMenuMain = new Main_1.default(); + _pageMenuUser = new User_1.default(); + } + } + function _closeAllMenus() { + document.querySelectorAll(".jsMobileButtonGroupNavigation.open, .jsMobileNavigation.open").forEach((menu) => { + menu.classList.remove("open"); + }); + if (_enabled && _dropdownMenu) { + closeDropdown(); + } + } + function _enableMobileSidebar() { + _mobileSidebarEnabled = true; + } + function _disableMobileSidebar() { + _mobileSidebarEnabled = false; + _sidebars.forEach(function (sidebar) { + sidebar.classList.remove("open"); + }); + } + function _setupMobileSidebar() { + _sidebars.forEach(function (sidebar) { + sidebar.addEventListener("mousedown", function (event) { + if (_mobileSidebarEnabled && event.target === sidebar) { + event.preventDefault(); + sidebar.classList.toggle("open"); } - } - }, - _enableMobileSidebar: function () { - _mobileSidebarEnabled = true; - }, - _disableMobileSidebar: function () { - _mobileSidebarEnabled = false; - _sidebars.forEach(function (sidebar) { - sidebar.classList.remove('open'); }); - }, - _setupMobileSidebar: function () { - _sidebars.forEach(function (sidebar) { - sidebar.addEventListener('mousedown', function (event) { - if (_mobileSidebarEnabled && event.target === sidebar) { - event.preventDefault(); - sidebar.classList.toggle('open'); - } - }); - }); - _mobileSidebarEnabled = true; - }, - _toggleMobileNavigation: function (message, quickOptions, navigation) { - if (_dropdownMenu === null) { - _dropdownMenu = elCreate('ul'); - _dropdownMenu.className = 'dropdownMenu'; - UiDropdownReusable.init('com.woltlab.wcf.jsMobileNavigation', _dropdownMenu); - _callbackCloseDropdown = function () { - _dropdownMenu.classList.remove('dropdownOpen'); - }; - } - else if (_dropdownMenu.classList.contains('dropdownOpen')) { - _callbackCloseDropdown(); - if (_dropdownMenuMessage === message) { - // toggle behavior - return; - } - } - _dropdownMenu.innerHTML = ''; - UiCloseOverlay.execute(); - this._rebuildMobileNavigation(navigation); - var previousNavigation = navigation.previousElementSibling; - if (previousNavigation && previousNavigation.classList.contains('messageFooterButtonsExtra')) { - var divider = elCreate('li'); - divider.className = 'dropdownDivider'; - _dropdownMenu.appendChild(divider); - this._rebuildMobileNavigation(previousNavigation); + }); + _mobileSidebarEnabled = true; + } + function closeDropdown() { + _dropdownMenu.classList.remove("dropdownOpen"); + } + function _toggleMobileNavigation(message, quickOptions, navigation) { + if (_dropdownMenu === null) { + _dropdownMenu = document.createElement("ul"); + _dropdownMenu.className = "dropdownMenu"; + UiDropdownReusable.init("com.woltlab.wcf.jsMobileNavigation", _dropdownMenu); + } + else if (_dropdownMenu.classList.contains("dropdownOpen")) { + closeDropdown(); + if (_dropdownMenuMessage === message) { + // toggle behavior + return; } - UiAlignment.set(_dropdownMenu, quickOptions, { - horizontal: 'right', - allowFlip: 'vertical' - }); - _dropdownMenu.classList.add('dropdownOpen'); - _dropdownMenuMessage = message; - }, - _setupLGTouchNavigation: function () { - _enabledLGTouchNavigation = true; - elBySelAll('.boxMenuHasChildren > a', null, function (element) { - element.addEventListener('touchstart', function (event) { - if (_enabledLGTouchNavigation && elAttr(element, 'aria-expanded') === 'false') { - event.preventDefault(); - elAttr(element, 'aria-expanded', 'true'); - // Register an new event listener after the touch ended, which is triggered once when an - // element on the page is pressed. This allows us to reset the touch status of the navigation - // entry when the entry is no longer open, so that it does not redirect to the page when you - // click it again. - element.addEventListener('touchend', function () { - document.body.addEventListener('touchstart', function () { - document.body.addEventListener('touchend', function (event) { - if (!DomUtil.contains(element.parentNode, event.target) && event.target !== element.parentNode) { - elAttr(element, 'aria-expanded', 'false'); - } - }, { - once: true - }); + } + _dropdownMenu.innerHTML = ""; + CloseOverlay_1.default.execute(); + _rebuildMobileNavigation(navigation); + const previousNavigation = navigation.previousElementSibling; + if (previousNavigation && previousNavigation.classList.contains("messageFooterButtonsExtra")) { + const divider = document.createElement("li"); + divider.className = "dropdownDivider"; + _dropdownMenu.appendChild(divider); + _rebuildMobileNavigation(previousNavigation); + } + UiAlignment.set(_dropdownMenu, quickOptions, { + horizontal: "right", + allowFlip: "vertical", + }); + _dropdownMenu.classList.add("dropdownOpen"); + _dropdownMenuMessage = message; + } + function _setupLGTouchNavigation() { + _enabledLGTouchNavigation = true; + document.querySelectorAll(".boxMenuHasChildren > a").forEach((element) => { + element.addEventListener("touchstart", function (event) { + if (_enabledLGTouchNavigation && element.getAttribute("aria-expanded") === "false") { + event.preventDefault(); + element.setAttribute("aria-expanded", "true"); + // Register an new event listener after the touch ended, which is triggered once when an + // element on the page is pressed. This allows us to reset the touch status of the navigation + // entry when the entry is no longer open, so that it does not redirect to the page when you + // click it again. + element.addEventListener("touchend", () => { + document.body.addEventListener("touchstart", () => { + document.body.addEventListener("touchend", (event) => { + const parent = element.parentElement; + const target = event.target; + if (!parent.contains(target) && target !== parent) { + element.setAttribute("aria-expanded", "false"); + } }, { - once: true + once: true, }); }, { - once: true + once: true, }); - } - }); + }, { once: true }); + } }); - }, - _enableLGTouchNavigation: function () { - _enabledLGTouchNavigation = true; - }, - _disableLGTouchNavigation: function () { - _enabledLGTouchNavigation = false; - }, - _rebuildMobileNavigation: function (navigation) { - elBySelAll('.button', navigation, function (button) { - if (button.classList.contains('ignoreMobileNavigation')) { - // The reaction button was hidden up until 5.2.2, but was enabled again in 5.2.3. This check - // exists to make sure that there is no unexpected behavior in 3rd party apps or plugins that - // used the same code and hid the reaction button via a CSS class in the template. - if (!button.classList.contains('reactButton')) { - return; - } + }); + } + function _enableLGTouchNavigation() { + _enabledLGTouchNavigation = true; + } + function _disableLGTouchNavigation() { + _enabledLGTouchNavigation = false; + } + function _rebuildMobileNavigation(navigation) { + navigation.querySelectorAll(".button").forEach((button) => { + if (button.classList.contains("ignoreMobileNavigation")) { + // The reaction button was hidden up until 5.2.2, but was enabled again in 5.2.3. This check + // exists to make sure that there is no unexpected behavior in 3rd party apps or plugins that + // used the same code and hid the reaction button via a CSS class in the template. + if (!button.classList.contains("reactButton")) { + return; } - var item = elCreate('li'); - if (button.classList.contains('active')) - item.className = 'active'; - item.innerHTML = '' + elBySel('span:not(.icon)', button).textContent + ''; - item.children[0].addEventListener('click', function (event) { - event.preventDefault(); - event.stopPropagation(); - if (button.nodeName === 'A') - button.click(); - else - Core.triggerEvent(button, 'click'); - _callbackCloseDropdown(); - }); - _dropdownMenu.appendChild(item); + } + const item = document.createElement("li"); + if (button.classList.contains("active")) { + item.className = "active"; + } + const label = button.querySelector("span:not(.icon)"); + item.innerHTML = `${label.textContent}`; + item.children[0].addEventListener("click", function (event) { + event.preventDefault(); + event.stopPropagation(); + if (button.nodeName === "A") { + button.click(); + } + else { + Core.triggerEvent(button, "click"); + } + closeDropdown(); + }); + _dropdownMenu.appendChild(item); + }); + } + /** + * Initializes the mobile UI. + */ + function setup(enableMobileMenu) { + _enableMobileMenu = enableMobileMenu; + document.querySelectorAll(".sidebar").forEach((sidebar) => { + _sidebars.push(sidebar); + }); + if (Environment.touch()) { + document.documentElement.classList.add("touch"); + } + if (Environment.platform() !== "desktop") { + document.documentElement.classList.add("mobile"); + } + const messageGroupList = document.querySelector(".messageGroupList"); + if (messageGroupList) { + _messageGroups = messageGroupList.getElementsByClassName("messageGroup"); + } + UiScreen.on("screen-md-down", { + match: enable, + unmatch: disable, + setup: _init, + }); + UiScreen.on("screen-sm-down", { + match: enableShadow, + unmatch: disableShadow, + setup: enableShadow, + }); + UiScreen.on("screen-md-down", { + match: _enableMobileSidebar, + unmatch: _disableMobileSidebar, + setup: _setupMobileSidebar, + }); + // On the large tablets (e.g. iPad Pro) the navigation is not usable, because there is not the mobile + // layout displayed, but the normal one for the desktop. The navigation reacts to a hover status if a + // menu item has several submenu items. Logically, this cannot be created with the tablet, so that we + // display the submenu here after a single click and only follow the link after another click. + if (Environment.touch() && (Environment.platform() === "ios" || Environment.platform() === "android")) { + UiScreen.on("screen-lg", { + match: _enableLGTouchNavigation, + unmatch: _disableLGTouchNavigation, + setup: _setupLGTouchNavigation, }); } - }; + } + exports.setup = setup; + /** + * Enables the mobile UI. + */ + function enable() { + _enabled = true; + if (_enableMobileMenu) { + _pageMenuMain.enable(); + _pageMenuUser.enable(); + } + } + exports.enable = enable; + /** + * Enables shadow links for larger click areas on messages. + */ + function enableShadow() { + if (_messageGroups) { + rebuildShadow(_messageGroups, ".messageGroupLink"); + } + } + exports.enableShadow = enableShadow; + /** + * Disables the mobile UI. + */ + function disable() { + _enabled = false; + if (_enableMobileMenu) { + _pageMenuMain.disable(); + _pageMenuUser.disable(); + } + } + exports.disable = disable; + /** + * Disables shadow links. + */ + function disableShadow() { + if (_messageGroups) { + removeShadow(_messageGroups); + } + if (_dropdownMenu) { + closeDropdown(); + } + } + exports.disableShadow = disableShadow; + function rebuildShadow(elements, linkSelector) { + Array.from(elements).forEach((element) => { + const parent = element.parentElement; + let shadow = parent.querySelector(".mobileLinkShadow"); + if (shadow === null) { + const link = element.querySelector(linkSelector); + if (link.href) { + shadow = document.createElement("a"); + shadow.className = "mobileLinkShadow"; + shadow.href = link.href; + parent.appendChild(shadow); + parent.classList.add("mobileLinkShadowContainer"); + } + } + }); + } + exports.rebuildShadow = rebuildShadow; + function removeShadow(elements) { + Array.from(elements).forEach((element) => { + const parent = element.parentElement; + if (parent.classList.contains("mobileLinkShadowContainer")) { + const shadow = parent.querySelector(".mobileLinkShadow"); + if (shadow !== null) { + shadow.remove(); + } + parent.classList.remove("mobileLinkShadowContainer"); + } + }); + } + exports.removeShadow = removeShadow; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Bootstrap.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Bootstrap.js index 30b6d3f74b..4ad46e7688 100644 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Bootstrap.js +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Bootstrap.js @@ -69,9 +69,7 @@ define( DateTimeRelative.setup(); DatePicker.init(); UiSimpleDropdown.setup(); - UiMobile.setup({ - enableMobileMenu: options.enableMobileMenu - }); + UiMobile.setup(options.enableMobileMenu); UiTabMenu.setup(); //UiFlexibleMenu.setup(); UiDialog.setup(); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Mobile.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Mobile.js deleted file mode 100644 index 11398a3270..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Mobile.js +++ /dev/null @@ -1,377 +0,0 @@ -/** - * Modifies the interface to provide a better usability for mobile devices. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Ui/Mobile - */ -define(['Core', 'Environment', 'EventHandler', 'Language', 'List', 'Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util', 'Ui/Alignment', 'Ui/CloseOverlay', 'Ui/Screen', './Page/Menu/Main', './Page/Menu/User', 'WoltLabSuite/Core/Ui/Dropdown/Reusable'], function (Core, Environment, EventHandler, Language, List, DomChangeListener, DomTraverse, DomUtil, UiAlignment, UiCloseOverlay, UiScreen, UiPageMenuMain, UiPageMenuUser, UiDropdownReusable) { - "use strict"; - var _buttonGroupNavigations = elByClass('buttonGroupNavigation'); - var _callbackCloseDropdown = null; - var _dropdownMenu = null; - var _dropdownMenuMessage = null; - var _enabled = false; - var _enabledLGTouchNavigation = false; - var _knownMessages = new List(); - var _main = null; - var _messages = elByClass('message'); - var _mobileSidebarEnabled = false; - var _options = {}; - var _pageMenuMain = null; - var _pageMenuUser = null; - var _messageGroups = null; - var _sidebars = []; - /** - * @exports WoltLabSuite/Core/Ui/Mobile - */ - return { - /** - * Initializes the mobile UI. - * - * @param {Object=} options initialization options - */ - setup: function (options) { - _options = Core.extend({ - enableMobileMenu: true - }, options); - _main = elById('main'); - elBySelAll('.sidebar', undefined, function (sidebar) { - _sidebars.push(sidebar); - }); - if (Environment.touch()) { - document.documentElement.classList.add('touch'); - } - if (Environment.platform() !== 'desktop') { - document.documentElement.classList.add('mobile'); - } - var messageGroupList = elBySel('.messageGroupList'); - if (messageGroupList) - _messageGroups = elByClass('messageGroup', messageGroupList); - UiScreen.on('screen-md-down', { - match: this.enable.bind(this), - unmatch: this.disable.bind(this), - setup: this._init.bind(this) - }); - UiScreen.on('screen-sm-down', { - match: this.enableShadow.bind(this), - unmatch: this.disableShadow.bind(this), - setup: this.enableShadow.bind(this) - }); - UiScreen.on('screen-md-down', { - match: this._enableMobileSidebar.bind(this), - unmatch: this._disableMobileSidebar.bind(this), - setup: this._setupMobileSidebar.bind(this) - }); - // On the large tablets (e.g. iPad Pro) the navigation is not usable, because there is not the mobile - // layout displayed, but the normal one for the desktop. The navigation reacts to a hover status if a - // menu item has several submenu items. Logically, this cannot be created with the tablet, so that we - // display the submenu here after a single click and only follow the link after another click. - if (Environment.touch() && (Environment.platform() === 'ios' || Environment.platform() === 'android')) { - UiScreen.on('screen-lg', { - match: this._enableLGTouchNavigation.bind(this), - unmatch: this._disableLGTouchNavigation.bind(this), - setup: this._setupLGTouchNavigation.bind(this) - }); - } - }, - /** - * Enables the mobile UI. - */ - enable: function () { - _enabled = true; - if (_options.enableMobileMenu) { - _pageMenuMain.enable(); - _pageMenuUser.enable(); - } - }, - /** - * Enables shadow links for larger click areas on messages. - */ - enableShadow: function () { - if (_messageGroups) - this.rebuildShadow(_messageGroups, '.messageGroupLink'); - }, - /** - * Disables the mobile UI. - */ - disable: function () { - _enabled = false; - if (_options.enableMobileMenu) { - _pageMenuMain.disable(); - _pageMenuUser.disable(); - } - }, - /** - * Disables shadow links. - */ - disableShadow: function () { - if (_messageGroups) - this.removeShadow(_messageGroups); - if (_dropdownMenu) - _callbackCloseDropdown(); - }, - _init: function () { - _enabled = true; - this._initSearchBar(); - this._initButtonGroupNavigation(); - this._initMessages(); - this._initMobileMenu(); - UiCloseOverlay.add('WoltLabSuite/Core/Ui/Mobile', this._closeAllMenus.bind(this)); - DomChangeListener.add('WoltLabSuite/Core/Ui/Mobile', (function () { - this._initButtonGroupNavigation(); - this._initMessages(); - }).bind(this)); - }, - _initSearchBar: function () { - var _searchBar = elById('pageHeaderSearch'); - var _searchInput = elById('pageHeaderSearchInput'); - var scrollTop = null; - EventHandler.add('com.woltlab.wcf.MainMenuMobile', 'more', function (data) { - if (data.identifier === 'com.woltlab.wcf.search') { - data.handler.close(true); - if (Environment.platform() === 'ios') { - scrollTop = document.body.scrollTop; - UiScreen.scrollDisable(); - } - _searchBar.style.setProperty('top', elById('pageHeader').offsetHeight + 'px', ''); - _searchBar.classList.add('open'); - _searchInput.focus(); - if (Environment.platform() === 'ios') { - document.body.scrollTop = 0; - } - } - }); - _main.addEventListener('click', function () { - if (_searchBar) - _searchBar.classList.remove('open'); - if (Environment.platform() === 'ios' && scrollTop !== null) { - UiScreen.scrollEnable(); - document.body.scrollTop = scrollTop; - scrollTop = null; - } - }); - }, - _initButtonGroupNavigation: function () { - for (var i = 0, length = _buttonGroupNavigations.length; i < length; i++) { - var navigation = _buttonGroupNavigations[i]; - if (navigation.classList.contains('jsMobileButtonGroupNavigation')) - continue; - else - navigation.classList.add('jsMobileButtonGroupNavigation'); - var list = elBySel('.buttonList', navigation); - if (list.childElementCount === 0) { - // ignore objects without options - continue; - } - navigation.parentNode.classList.add('hasMobileNavigation'); - var button = elCreate('a'); - button.className = 'dropdownLabel'; - var span = elCreate('span'); - span.className = 'icon icon24 fa-ellipsis-v'; - button.appendChild(span); - (function (navigation, button, list) { - button.addEventListener('click', function (event) { - event.preventDefault(); - event.stopPropagation(); - navigation.classList.toggle('open'); - }); - list.addEventListener('click', function (event) { - event.stopPropagation(); - navigation.classList.remove('open'); - }); - })(navigation, button, list); - navigation.insertBefore(button, navigation.firstChild); - } - }, - _initMessages: function () { - Array.prototype.forEach.call(_messages, (function (message) { - if (_knownMessages.has(message)) { - return; - } - var navigation = elBySel('.jsMobileNavigation', message); - if (navigation) { - navigation.addEventListener('click', function (event) { - event.stopPropagation(); - // mimic dropdown behavior - window.setTimeout(function () { - navigation.classList.remove('open'); - }, 10); - }); - var quickOptions = elBySel('.messageQuickOptions', message); - if (quickOptions && navigation.childElementCount) { - quickOptions.classList.add('active'); - quickOptions.addEventListener('click', (function (event) { - if (_enabled && UiScreen.is('screen-sm-down') && event.target.nodeName !== 'LABEL' && event.target.nodeName !== 'INPUT') { - event.preventDefault(); - event.stopPropagation(); - this._toggleMobileNavigation(message, quickOptions, navigation); - } - }).bind(this)); - } - } - _knownMessages.add(message); - }).bind(this)); - }, - _initMobileMenu: function () { - if (_options.enableMobileMenu) { - _pageMenuMain = new UiPageMenuMain(); - _pageMenuUser = new UiPageMenuUser(); - } - }, - _closeAllMenus: function () { - elBySelAll('.jsMobileButtonGroupNavigation.open, .jsMobileNavigation.open', null, function (menu) { - menu.classList.remove('open'); - }); - if (_enabled && _dropdownMenu) - _callbackCloseDropdown(); - }, - rebuildShadow: function (elements, linkSelector) { - var element, parent, shadow; - for (var i = 0, length = elements.length; i < length; i++) { - element = elements[i]; - parent = element.parentNode; - shadow = DomTraverse.childByClass(parent, 'mobileLinkShadow'); - if (shadow === null) { - if (elBySel(linkSelector, element).href) { - shadow = elCreate('a'); - shadow.className = 'mobileLinkShadow'; - shadow.href = elBySel(linkSelector, element).href; - parent.appendChild(shadow); - parent.classList.add('mobileLinkShadowContainer'); - } - } - } - }, - removeShadow: function (elements) { - var element, parent, shadow; - for (var i = 0, length = elements.length; i < length; i++) { - element = elements[i]; - parent = element.parentNode; - if (parent.classList.contains('mobileLinkShadowContainer')) { - shadow = DomTraverse.childByClass(parent, 'mobileLinkShadow'); - if (shadow !== null) { - elRemove(shadow); - } - parent.classList.remove('mobileLinkShadowContainer'); - } - } - }, - _enableMobileSidebar: function () { - _mobileSidebarEnabled = true; - }, - _disableMobileSidebar: function () { - _mobileSidebarEnabled = false; - _sidebars.forEach(function (sidebar) { - sidebar.classList.remove('open'); - }); - }, - _setupMobileSidebar: function () { - _sidebars.forEach(function (sidebar) { - sidebar.addEventListener('mousedown', function (event) { - if (_mobileSidebarEnabled && event.target === sidebar) { - event.preventDefault(); - sidebar.classList.toggle('open'); - } - }); - }); - _mobileSidebarEnabled = true; - }, - _toggleMobileNavigation: function (message, quickOptions, navigation) { - if (_dropdownMenu === null) { - _dropdownMenu = elCreate('ul'); - _dropdownMenu.className = 'dropdownMenu'; - UiDropdownReusable.init('com.woltlab.wcf.jsMobileNavigation', _dropdownMenu); - _callbackCloseDropdown = function () { - _dropdownMenu.classList.remove('dropdownOpen'); - }; - } - else if (_dropdownMenu.classList.contains('dropdownOpen')) { - _callbackCloseDropdown(); - if (_dropdownMenuMessage === message) { - // toggle behavior - return; - } - } - _dropdownMenu.innerHTML = ''; - UiCloseOverlay.execute(); - this._rebuildMobileNavigation(navigation); - var previousNavigation = navigation.previousElementSibling; - if (previousNavigation && previousNavigation.classList.contains('messageFooterButtonsExtra')) { - var divider = elCreate('li'); - divider.className = 'dropdownDivider'; - _dropdownMenu.appendChild(divider); - this._rebuildMobileNavigation(previousNavigation); - } - UiAlignment.set(_dropdownMenu, quickOptions, { - horizontal: 'right', - allowFlip: 'vertical' - }); - _dropdownMenu.classList.add('dropdownOpen'); - _dropdownMenuMessage = message; - }, - _setupLGTouchNavigation: function () { - _enabledLGTouchNavigation = true; - elBySelAll('.boxMenuHasChildren > a', null, function (element) { - element.addEventListener('touchstart', function (event) { - if (_enabledLGTouchNavigation && elAttr(element, 'aria-expanded') === 'false') { - event.preventDefault(); - elAttr(element, 'aria-expanded', 'true'); - // Register an new event listener after the touch ended, which is triggered once when an - // element on the page is pressed. This allows us to reset the touch status of the navigation - // entry when the entry is no longer open, so that it does not redirect to the page when you - // click it again. - element.addEventListener('touchend', function () { - document.body.addEventListener('touchstart', function () { - document.body.addEventListener('touchend', function (event) { - if (!DomUtil.contains(element.parentNode, event.target) && event.target !== element.parentNode) { - elAttr(element, 'aria-expanded', 'false'); - } - }, { - once: true - }); - }, { - once: true - }); - }, { - once: true - }); - } - }); - }); - }, - _enableLGTouchNavigation: function () { - _enabledLGTouchNavigation = true; - }, - _disableLGTouchNavigation: function () { - _enabledLGTouchNavigation = false; - }, - _rebuildMobileNavigation: function (navigation) { - elBySelAll('.button', navigation, function (button) { - if (button.classList.contains('ignoreMobileNavigation')) { - // The reaction button was hidden up until 5.2.2, but was enabled again in 5.2.3. This check - // exists to make sure that there is no unexpected behavior in 3rd party apps or plugins that - // used the same code and hid the reaction button via a CSS class in the template. - if (!button.classList.contains('reactButton')) { - return; - } - } - var item = elCreate('li'); - if (button.classList.contains('active')) - item.className = 'active'; - item.innerHTML = '' + elBySel('span:not(.icon)', button).textContent + ''; - item.children[0].addEventListener('click', function (event) { - event.preventDefault(); - event.stopPropagation(); - if (button.nodeName === 'A') - button.click(); - else - Core.triggerEvent(button, 'click'); - _callbackCloseDropdown(); - }); - _dropdownMenu.appendChild(item); - }); - } - }; -}); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Mobile.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Mobile.ts new file mode 100644 index 0000000000..a82d20cf31 --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Mobile.ts @@ -0,0 +1,446 @@ +/** + * Modifies the interface to provide a better usability for mobile devices. + * + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/Mobile + */ + +import * as Core from "../Core"; +import DomChangeListener from "../Dom/Change/Listener"; +import * as Environment from "../Environment"; +import * as EventHandler from "../Event/Handler"; +import * as UiAlignment from "./Alignment"; +import UiCloseOverlay from "./CloseOverlay"; +import * as UiDropdownReusable from "./Dropdown/Reusable"; +import UiPageMenuMain from "./Page/Menu/Main"; +import UiPageMenuUser from "./Page/Menu/User"; +import * as UiScreen from "./Screen"; + +interface MainMenuMorePayload { + identifier: string; + handler: UiPageMenuMain; +} + +let _dropdownMenu: HTMLUListElement | null = null; +let _dropdownMenuMessage = null; +let _enabled = false; +let _enabledLGTouchNavigation = false; +let _enableMobileMenu = false; +const _knownMessages = new WeakSet(); +let _mobileSidebarEnabled = false; +let _pageMenuMain: UiPageMenuMain; +let _pageMenuUser: UiPageMenuUser; +let _messageGroups: HTMLCollection | null = null; +const _sidebars: HTMLElement[] = []; + +function _init(): void { + _enabled = true; + + initSearchBar(); + _initButtonGroupNavigation(); + _initMessages(); + _initMobileMenu(); + + UiCloseOverlay.add("WoltLabSuite/Core/Ui/Mobile", _closeAllMenus); + DomChangeListener.add("WoltLabSuite/Core/Ui/Mobile", () => { + _initButtonGroupNavigation(); + _initMessages(); + }); +} + +function initSearchBar(): void { + const searchBar = document.getElementById("pageHeaderSearch")!; + const searchInput = document.getElementById("pageHeaderSearchInput")!; + + let scrollTop: number | null = null; + EventHandler.add("com.woltlab.wcf.MainMenuMobile", "more", (data: MainMenuMorePayload) => { + if (data.identifier === "com.woltlab.wcf.search") { + data.handler.close(); + + if (Environment.platform() === "ios") { + scrollTop = document.body.scrollTop; + UiScreen.scrollDisable(); + } + + const pageHeader = document.getElementById("pageHeader")!; + searchBar.style.setProperty("top", `${pageHeader.offsetHeight}px`, ""); + searchBar.classList.add("open"); + searchInput.focus(); + + if (Environment.platform() === "ios") { + document.body.scrollTop = 0; + } + } + }); + + document.getElementById("main")!.addEventListener("click", () => { + if (searchBar) { + searchBar.classList.remove("open"); + } + + if (Environment.platform() === "ios" && scrollTop) { + UiScreen.scrollEnable(); + document.body.scrollTop = scrollTop; + scrollTop = null; + } + }); +} + +function _initButtonGroupNavigation(): void { + document.querySelectorAll(".buttonGroupNavigation").forEach((navigation) => { + if (navigation.classList.contains("jsMobileButtonGroupNavigation")) { + return; + } else { + navigation.classList.add("jsMobileButtonGroupNavigation"); + } + + const list = navigation.querySelector(".buttonList") as HTMLUListElement; + if (list.childElementCount === 0) { + // ignore objects without options + return; + } + + navigation.parentElement!.classList.add("hasMobileNavigation"); + + const button = document.createElement("a"); + button.className = "dropdownLabel"; + const span = document.createElement("span"); + span.className = "icon icon24 fa-ellipsis-v"; + button.appendChild(span); + button.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + + navigation.classList.toggle("open"); + }); + + list.addEventListener("click", function (event) { + event.stopPropagation(); + navigation.classList.remove("open"); + }); + + navigation.insertBefore(button, navigation.firstChild); + }); +} + +function _initMessages(): void { + document.querySelectorAll(".message").forEach((message: HTMLElement) => { + if (_knownMessages.has(message)) { + return; + } + + const navigation = message.querySelector(".jsMobileNavigation") as HTMLAnchorElement; + if (navigation) { + navigation.addEventListener("click", (event) => { + event.stopPropagation(); + + // mimic dropdown behavior + window.setTimeout(() => { + navigation.classList.remove("open"); + }, 10); + }); + + const quickOptions = message.querySelector(".messageQuickOptions"); + if (quickOptions && navigation.childElementCount) { + quickOptions.classList.add("active"); + quickOptions.addEventListener("click", (event) => { + const target = event.target as HTMLElement; + + if (_enabled && UiScreen.is("screen-sm-down") && target.nodeName !== "LABEL" && target.nodeName !== "INPUT") { + event.preventDefault(); + event.stopPropagation(); + + _toggleMobileNavigation(message, quickOptions, navigation); + } + }); + } + } + _knownMessages.add(message); + }); +} + +function _initMobileMenu(): void { + if (_enableMobileMenu) { + _pageMenuMain = new UiPageMenuMain(); + _pageMenuUser = new UiPageMenuUser(); + } +} + +function _closeAllMenus(): void { + document.querySelectorAll(".jsMobileButtonGroupNavigation.open, .jsMobileNavigation.open").forEach((menu) => { + menu.classList.remove("open"); + }); + + if (_enabled && _dropdownMenu) { + closeDropdown(); + } +} + +function _enableMobileSidebar(): void { + _mobileSidebarEnabled = true; +} + +function _disableMobileSidebar(): void { + _mobileSidebarEnabled = false; + _sidebars.forEach(function (sidebar) { + sidebar.classList.remove("open"); + }); +} + +function _setupMobileSidebar(): void { + _sidebars.forEach(function (sidebar) { + sidebar.addEventListener("mousedown", function (event) { + if (_mobileSidebarEnabled && event.target === sidebar) { + event.preventDefault(); + sidebar.classList.toggle("open"); + } + }); + }); + _mobileSidebarEnabled = true; +} + +function closeDropdown(): void { + _dropdownMenu!.classList.remove("dropdownOpen"); +} + +function _toggleMobileNavigation(message, quickOptions, navigation): void { + if (_dropdownMenu === null) { + _dropdownMenu = document.createElement("ul"); + _dropdownMenu.className = "dropdownMenu"; + UiDropdownReusable.init("com.woltlab.wcf.jsMobileNavigation", _dropdownMenu); + } else if (_dropdownMenu.classList.contains("dropdownOpen")) { + closeDropdown(); + if (_dropdownMenuMessage === message) { + // toggle behavior + return; + } + } + _dropdownMenu.innerHTML = ""; + UiCloseOverlay.execute(); + _rebuildMobileNavigation(navigation); + const previousNavigation = navigation.previousElementSibling; + if (previousNavigation && previousNavigation.classList.contains("messageFooterButtonsExtra")) { + const divider = document.createElement("li"); + divider.className = "dropdownDivider"; + _dropdownMenu.appendChild(divider); + _rebuildMobileNavigation(previousNavigation); + } + UiAlignment.set(_dropdownMenu, quickOptions, { + horizontal: "right", + allowFlip: "vertical", + }); + _dropdownMenu.classList.add("dropdownOpen"); + _dropdownMenuMessage = message; +} + +function _setupLGTouchNavigation(): void { + _enabledLGTouchNavigation = true; + document.querySelectorAll(".boxMenuHasChildren > a").forEach((element: HTMLElement) => { + element.addEventListener("touchstart", function (event) { + if (_enabledLGTouchNavigation && element.getAttribute("aria-expanded") === "false") { + event.preventDefault(); + + element.setAttribute("aria-expanded", "true"); + + // Register an new event listener after the touch ended, which is triggered once when an + // element on the page is pressed. This allows us to reset the touch status of the navigation + // entry when the entry is no longer open, so that it does not redirect to the page when you + // click it again. + element.addEventListener( + "touchend", + () => { + document.body.addEventListener( + "touchstart", + () => { + document.body.addEventListener( + "touchend", + (event) => { + const parent = element.parentElement!; + const target = event.target as HTMLElement; + if (!parent.contains(target) && target !== parent) { + element.setAttribute("aria-expanded", "false"); + } + }, + { + once: true, + }, + ); + }, + { + once: true, + }, + ); + }, + { once: true }, + ); + } + }); + }); +} + +function _enableLGTouchNavigation(): void { + _enabledLGTouchNavigation = true; +} + +function _disableLGTouchNavigation(): void { + _enabledLGTouchNavigation = false; +} + +function _rebuildMobileNavigation(navigation: HTMLElement): void { + navigation.querySelectorAll(".button").forEach((button: HTMLElement) => { + if (button.classList.contains("ignoreMobileNavigation")) { + // The reaction button was hidden up until 5.2.2, but was enabled again in 5.2.3. This check + // exists to make sure that there is no unexpected behavior in 3rd party apps or plugins that + // used the same code and hid the reaction button via a CSS class in the template. + if (!button.classList.contains("reactButton")) { + return; + } + } + + const item = document.createElement("li"); + if (button.classList.contains("active")) { + item.className = "active"; + } + + const label = button.querySelector("span:not(.icon)")!; + item.innerHTML = `${label.textContent!}`; + item.children[0].addEventListener("click", function (event) { + event.preventDefault(); + event.stopPropagation(); + if (button.nodeName === "A") { + button.click(); + } else { + Core.triggerEvent(button, "click"); + } + closeDropdown(); + }); + _dropdownMenu!.appendChild(item); + }); +} + +/** + * Initializes the mobile UI. + */ +export function setup(enableMobileMenu: boolean): void { + _enableMobileMenu = enableMobileMenu; + document.querySelectorAll(".sidebar").forEach((sidebar: HTMLElement) => { + _sidebars.push(sidebar); + }); + + if (Environment.touch()) { + document.documentElement.classList.add("touch"); + } + if (Environment.platform() !== "desktop") { + document.documentElement.classList.add("mobile"); + } + + const messageGroupList = document.querySelector(".messageGroupList"); + if (messageGroupList) { + _messageGroups = messageGroupList.getElementsByClassName("messageGroup"); + } + + UiScreen.on("screen-md-down", { + match: enable, + unmatch: disable, + setup: _init, + }); + UiScreen.on("screen-sm-down", { + match: enableShadow, + unmatch: disableShadow, + setup: enableShadow, + }); + UiScreen.on("screen-md-down", { + match: _enableMobileSidebar, + unmatch: _disableMobileSidebar, + setup: _setupMobileSidebar, + }); + + // On the large tablets (e.g. iPad Pro) the navigation is not usable, because there is not the mobile + // layout displayed, but the normal one for the desktop. The navigation reacts to a hover status if a + // menu item has several submenu items. Logically, this cannot be created with the tablet, so that we + // display the submenu here after a single click and only follow the link after another click. + if (Environment.touch() && (Environment.platform() === "ios" || Environment.platform() === "android")) { + UiScreen.on("screen-lg", { + match: _enableLGTouchNavigation, + unmatch: _disableLGTouchNavigation, + setup: _setupLGTouchNavigation, + }); + } +} + +/** + * Enables the mobile UI. + */ +export function enable(): void { + _enabled = true; + if (_enableMobileMenu) { + _pageMenuMain.enable(); + _pageMenuUser.enable(); + } +} + +/** + * Enables shadow links for larger click areas on messages. + */ +export function enableShadow(): void { + if (_messageGroups) { + rebuildShadow(_messageGroups, ".messageGroupLink"); + } +} + +/** + * Disables the mobile UI. + */ +export function disable(): void { + _enabled = false; + if (_enableMobileMenu) { + _pageMenuMain.disable(); + _pageMenuUser.disable(); + } +} + +/** + * Disables shadow links. + */ +export function disableShadow(): void { + if (_messageGroups) { + removeShadow(_messageGroups); + } + if (_dropdownMenu) { + closeDropdown(); + } +} + +export function rebuildShadow(elements: HTMLElement[] | HTMLCollection, linkSelector: string): void { + Array.from(elements).forEach((element) => { + const parent = element.parentElement as HTMLElement; + + let shadow = parent.querySelector(".mobileLinkShadow") as HTMLAnchorElement; + if (shadow === null) { + const link = element.querySelector(linkSelector) as HTMLAnchorElement; + if (link.href) { + shadow = document.createElement("a"); + shadow.className = "mobileLinkShadow"; + shadow.href = link.href; + parent.appendChild(shadow); + parent.classList.add("mobileLinkShadowContainer"); + } + } + }); +} + +export function removeShadow(elements: HTMLElement[] | HTMLCollection): void { + Array.from(elements).forEach((element) => { + const parent = element.parentElement!; + if (parent.classList.contains("mobileLinkShadowContainer")) { + const shadow = parent.querySelector(".mobileLinkShadow"); + if (shadow !== null) { + shadow.remove(); + } + + parent.classList.remove("mobileLinkShadowContainer"); + } + }); +} -- 2.20.1