/**
* Prepends an element to a parent element.
*
- * @deprecated 5.3 Use `parent.insertBefore(element, parent.firstChild)` instead.
+ * @deprecated 5.3 Use `parent.insertAdjacentElement('afterbegin', element)` instead.
*/
prepend(element, parent) {
- parent.insertBefore(element, parent.firstChild);
+ parent.insertAdjacentElement('afterbegin', element);
},
/**
* Inserts an element after an existing element.
*
- * @deprecated 5.3 Use `element.parentNode.insertBefore(newElement, element.nextSibling)` instead.
+ * @deprecated 5.3 Use `element.insertAdjacentElement('afterend', newElement)` instead.
*/
insertAfter(newElement, element) {
- if (element.parentNode === null) {
- throw new Error('The target element has no parent.');
- }
- element.parentNode.insertBefore(newElement, element.nextSibling);
+ element.insertAdjacentElement('afterend', newElement);
},
/**
* Applies a list of CSS properties to an element.
/**
* Provides a touch-friendly fullscreen 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/Menu/Abstract
+ * @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/Menu/Abstract
*/
-define(['Core', 'Environment', 'EventHandler', 'Language', 'ObjectMap', 'Dom/Traverse', 'Dom/Util', 'Ui/Screen'], function (Core, Environment, EventHandler, Language, ObjectMap, DomTraverse, DomUtil, UiScreen) {
+define(["require", "exports", "tslib", "../../../Core", "../../../Environment", "../../../Event/Handler", "../../../Language", "../../../Dom/Traverse", "../../Screen"], function (require, exports, tslib_1, Core, Environment, EventHandler, Language, DomTraverse, UiScreen) {
"use strict";
- var _pageContainer = elById('pageContainer');
+ Core = tslib_1.__importStar(Core);
+ Environment = tslib_1.__importStar(Environment);
+ EventHandler = tslib_1.__importStar(EventHandler);
+ Language = tslib_1.__importStar(Language);
+ DomTraverse = tslib_1.__importStar(DomTraverse);
+ UiScreen = tslib_1.__importStar(UiScreen);
+ const _pageContainer = document.getElementById('pageContainer');
/**
* Which edge of the menu is touched? Empty string
* if no menu is currently touched.
*
* One 'left', 'right' or ''.
*/
- var _androidTouching = '';
- /**
- * @param {string} eventIdentifier event namespace
- * @param {string} elementId menu element id
- * @param {string} buttonSelector CSS selector for toggle button
- * @constructor
- */
- function UiPageMenuAbstract(eventIdentifier, elementId, buttonSelector) { this.init(eventIdentifier, elementId, buttonSelector); }
- UiPageMenuAbstract.prototype = {
- /**
- * Initializes a touch-friendly fullscreen menu.
- *
- * @param {string} eventIdentifier event namespace
- * @param {string} elementId menu element id
- * @param {string} buttonSelector CSS selector for toggle button
- */
- init: function (eventIdentifier, elementId, buttonSelector) {
- if (elData(document.body, 'template') === 'packageInstallationSetup') {
+ let _androidTouching = '';
+ class UiPageMenuAbstract {
+ constructor(eventIdentifier, elementId, buttonSelector) {
+ this.activeList = [];
+ this.depth = 0;
+ this.enabled = true;
+ this.items = new Map();
+ this.removeActiveList = false;
+ if (document.body.dataset.template === 'packageInstallationSetup') {
// work-around for WCFSetup on mobile
return;
}
- this._activeList = [];
- this._depth = 0;
- this._enabled = true;
- this._eventIdentifier = eventIdentifier;
- this._items = new ObjectMap();
- this._menu = elById(elementId);
- this._removeActiveList = false;
- var callbackOpen = this.open.bind(this);
- this._button = elBySel(buttonSelector);
- this._button.addEventListener(WCF_CLICK_EVENT, callbackOpen);
- this._initItems();
- this._initHeader();
- EventHandler.add(this._eventIdentifier, 'open', callbackOpen);
- EventHandler.add(this._eventIdentifier, 'close', this.close.bind(this));
- EventHandler.add(this._eventIdentifier, 'updateButtonState', this._updateButtonState.bind(this));
- var itemList, itemLists = elByClass('menuOverlayItemList', this._menu);
- this._menu.addEventListener('animationend', (function () {
- if (!this._menu.classList.contains('open')) {
- for (var i = 0, length = itemLists.length; i < length; i++) {
- itemList = itemLists[i];
+ this.eventIdentifier = eventIdentifier;
+ this.menu = document.getElementById(elementId);
+ const callbackOpen = this.open.bind(this);
+ this.button = document.querySelector(buttonSelector);
+ this.button.addEventListener('click', callbackOpen);
+ this.initItems();
+ this.initHeader();
+ EventHandler.add(this.eventIdentifier, 'open', callbackOpen);
+ EventHandler.add(this.eventIdentifier, 'close', this.close.bind(this));
+ EventHandler.add(this.eventIdentifier, 'updateButtonState', this.updateButtonState.bind(this));
+ this.menu.addEventListener('animationend', () => {
+ if (!this.menu.classList.contains('open')) {
+ this.menu.querySelectorAll('.menuOverlayItemList').forEach(itemList => {
// force the main list to be displayed
- itemList.classList.remove('active');
- itemList.classList.remove('hidden');
- }
+ itemList.classList.remove('active', 'hidden');
+ });
}
- }).bind(this));
- this._menu.children[0].addEventListener('transitionend', (function () {
- this._menu.classList.add('allowScroll');
- if (this._removeActiveList) {
- this._removeActiveList = false;
- var list = this._activeList.pop();
+ });
+ this.menu.children[0].addEventListener('transitionend', () => {
+ this.menu.classList.add('allowScroll');
+ if (this.removeActiveList) {
+ this.removeActiveList = false;
+ const list = this.activeList.pop();
if (list) {
list.classList.remove('activeList');
}
}
- }).bind(this));
- var backdrop = elCreate('div');
+ });
+ const backdrop = document.createElement('div');
backdrop.className = 'menuOverlayMobileBackdrop';
- backdrop.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
- DomUtil.insertAfter(backdrop, this._menu);
- this._updateButtonState();
+ backdrop.addEventListener('click', this.close.bind(this));
+ this.menu.insertAdjacentElement('afterend', backdrop);
+ this.menu.parentElement.insertBefore(backdrop, this.menu.nextSibling);
+ this.updateButtonState();
if (Environment.platform() === 'android') {
- this._initializeAndroid();
+ this.initializeAndroid();
}
- },
+ }
/**
* Opens the menu.
- *
- * @param {Event} event event object
- * @return {boolean} true if menu has been opened
*/
- open: function (event) {
- if (!this._enabled) {
+ open(event) {
+ if (!this.enabled) {
return false;
}
if (event instanceof Event) {
event.preventDefault();
}
- this._menu.classList.add('open');
- this._menu.classList.add('allowScroll');
- this._menu.children[0].classList.add('activeList');
+ this.menu.classList.add('open');
+ this.menu.classList.add('allowScroll');
+ this.menu.children[0].classList.add('activeList');
UiScreen.scrollDisable();
- _pageContainer.classList.add('menuOverlay-' + this._menu.id);
+ _pageContainer.classList.add('menuOverlay-' + this.menu.id);
UiScreen.pageOverlayOpen();
return true;
- },
+ }
/**
* Closes the menu.
- *
- * @param {(Event|boolean)} event event object or boolean true to force close the menu
- * @return {boolean} true if menu was open
*/
- close: function (event) {
+ close(event) {
if (event instanceof Event) {
event.preventDefault();
}
- if (this._menu.classList.contains('open')) {
- this._menu.classList.remove('open');
+ if (this.menu.classList.contains('open')) {
+ this.menu.classList.remove('open');
UiScreen.scrollEnable();
UiScreen.pageOverlayClose();
- _pageContainer.classList.remove('menuOverlay-' + this._menu.id);
+ _pageContainer.classList.remove('menuOverlay-' + this.menu.id);
return true;
}
return false;
- },
+ }
/**
* Enables the touch menu.
*/
- enable: function () {
- this._enabled = true;
- },
+ enable() {
+ this.enabled = true;
+ }
/**
* Disables the touch menu.
*/
- disable: function () {
- this._enabled = false;
- this.close(true);
- },
+ disable() {
+ this.enabled = false;
+ this.close();
+ }
/**
* Initializes the Android Touch Menu.
*/
- _initializeAndroid: function () {
- var appearsAt, backdrop, touchStart;
- /** @const */ var AT_EDGE = 20;
- /** @const */ var MOVED_HORIZONTALLY = 5;
- /** @const */ var MOVED_VERTICALLY = 20;
+ initializeAndroid() {
// specify on which side of the page the menu appears
- switch (this._menu.id) {
+ let appearsAt;
+ switch (this.menu.id) {
case 'pageUserMenuMobile':
appearsAt = 'right';
break;
default:
return;
}
- backdrop = this._menu.nextElementSibling;
+ const backdrop = this.menu.nextElementSibling;
// horizontal position of the touch start
- touchStart = null;
- document.addEventListener('touchstart', (function (event) {
- var touches, isOpen, isLeftEdge, isRightEdge;
- touches = event.touches;
- isOpen = this._menu.classList.contains('open');
+ let touchStart = undefined;
+ document.addEventListener('touchstart', event => {
+ const touches = event.touches;
+ let isLeftEdge;
+ let isRightEdge;
+ const isOpen = this.menu.classList.contains('open');
// check whether we touch the edges of the menu
if (appearsAt === 'left') {
- isLeftEdge = !isOpen && (touches[0].clientX < AT_EDGE);
- isRightEdge = isOpen && (Math.abs(this._menu.offsetWidth - touches[0].clientX) < AT_EDGE);
+ isLeftEdge = !isOpen && (touches[0].clientX < 20 /* AtEdge */);
+ isRightEdge = isOpen && (Math.abs(this.menu.offsetWidth - touches[0].clientX) < 20 /* AtEdge */);
}
- else if (appearsAt === 'right') {
- isLeftEdge = isOpen && (Math.abs(document.body.clientWidth - this._menu.offsetWidth - touches[0].clientX) < AT_EDGE);
- isRightEdge = !isOpen && ((document.body.clientWidth - touches[0].clientX) < AT_EDGE);
+ else {
+ isLeftEdge = isOpen && (Math.abs(document.body.clientWidth - this.menu.offsetWidth - touches[0].clientX) < 20 /* AtEdge */);
+ isRightEdge = !isOpen && ((document.body.clientWidth - touches[0].clientX) < 20 /* AtEdge */);
}
// abort if more than one touch
if (touches.length > 1) {
return;
}
// break if a touch is in progress
- if (_androidTouching)
+ if (_androidTouching) {
return;
+ }
// break if no edge has been touched
- if (!isLeftEdge && !isRightEdge)
+ if (!isLeftEdge && !isRightEdge) {
return;
+ }
// break if a different menu is open
if (UiScreen.pageOverlayIsActive()) {
- var found = false;
- for (var i = 0; i < _pageContainer.classList.length; i++) {
- if (_pageContainer.classList[i] === 'menuOverlay-' + this._menu.id) {
- found = true;
- }
- }
- if (!found)
+ const found = _pageContainer.classList.contains(`menuOverlay-${this.menu.id}`);
+ if (!found) {
return;
+ }
}
// break if redactor is in use
- if (document.documentElement.classList.contains('redactorActive'))
+ if (document.documentElement.classList.contains('redactorActive')) {
return;
+ }
touchStart = {
x: touches[0].clientX,
- y: touches[0].clientY
+ y: touches[0].clientY,
};
if (isLeftEdge)
_androidTouching = 'left';
if (isRightEdge)
_androidTouching = 'right';
- }).bind(this));
- document.addEventListener('touchend', (function (event) {
+ });
+ document.addEventListener('touchend', event => {
// break if we did not start a touch
- if (!_androidTouching || touchStart === null)
+ if (!_androidTouching || !touchStart) {
return;
+ }
// break if the menu did not even start opening
- if (!this._menu.classList.contains('open')) {
+ if (!this.menu.classList.contains('open')) {
// reset
- touchStart = null;
+ touchStart = undefined;
_androidTouching = '';
return;
}
// last known position of the finger
- var position;
+ let position;
if (event) {
position = event.changedTouches[0].clientX;
}
position = touchStart.x;
}
// clean up touch styles
- this._menu.classList.add('androidMenuTouchEnd');
- this._menu.style.removeProperty('transform');
+ this.menu.classList.add('androidMenuTouchEnd');
+ this.menu.style.removeProperty('transform');
backdrop.style.removeProperty(appearsAt);
- this._menu.addEventListener('transitionend', (function () {
- this._menu.classList.remove('androidMenuTouchEnd');
- }).bind(this), { once: true });
+ this.menu.addEventListener('transitionend', () => {
+ this.menu.classList.remove('androidMenuTouchEnd');
+ }, { once: true });
// check whether the user moved the finger far enough
if (appearsAt === 'left') {
if (_androidTouching === 'left' && position < (touchStart.x + 100))
if (_androidTouching === 'right' && position < (touchStart.x - 100))
this.close();
}
- else if (appearsAt === 'right') {
+ else {
if (_androidTouching === 'left' && position > (touchStart.x + 100))
this.close();
if (_androidTouching === 'right' && position > (touchStart.x - 100))
this.close();
}
// reset
- touchStart = null;
+ touchStart = undefined;
_androidTouching = '';
- }).bind(this));
- document.addEventListener('touchmove', (function (event) {
+ });
+ document.addEventListener('touchmove', event => {
// break if we did not start a touch
- if (!_androidTouching || touchStart === null)
+ if (!_androidTouching || !touchStart) {
return;
- var touches = event.touches;
+ }
+ const touches = event.touches;
// check whether the user started moving in the correct direction
// this avoids false positives, in case the user just wanted to tap
- var movedFromEdge = false, movedVertically = false;
+ let movedFromEdge = false;
if (_androidTouching === 'left')
- movedFromEdge = touches[0].clientX > (touchStart.x + MOVED_HORIZONTALLY);
+ movedFromEdge = touches[0].clientX > (touchStart.x + 5 /* MovedHorizontally */);
if (_androidTouching === 'right')
- movedFromEdge = touches[0].clientX < (touchStart.x - MOVED_HORIZONTALLY);
- movedVertically = Math.abs(touches[0].clientY - touchStart.y) > MOVED_VERTICALLY;
- var isOpen = this._menu.classList.contains('open');
+ movedFromEdge = touches[0].clientX < (touchStart.x - 5 /* MovedHorizontally */);
+ const movedVertically = Math.abs(touches[0].clientY - touchStart.y) > 20 /* MovedVertically */;
+ let isOpen = this.menu.classList.contains('open');
if (!isOpen && movedFromEdge && !movedVertically) {
// the menu is not yet open, but the user moved into the right direction
this.open();
}
if (isOpen) {
// update CSS to the new finger position
- var position = touches[0].clientX;
+ let position = touches[0].clientX;
if (appearsAt === 'right')
position = document.body.clientWidth - position;
- if (position > this._menu.offsetWidth)
- position = this._menu.offsetWidth;
+ if (position > this.menu.offsetWidth)
+ position = this.menu.offsetWidth;
if (position < 0)
position = 0;
- this._menu.style.setProperty('transform', 'translateX(' + (appearsAt === 'left' ? 1 : -1) * (position - this._menu.offsetWidth) + 'px)');
- backdrop.style.setProperty(appearsAt, Math.min(this._menu.offsetWidth, position) + 'px');
+ this.menu.style.setProperty('transform', 'translateX(' + (appearsAt === 'left' ? 1 : -1) * (position - this.menu.offsetWidth) + 'px)');
+ backdrop.style.setProperty(appearsAt, Math.min(this.menu.offsetWidth, position) + 'px');
}
- }).bind(this));
- },
+ });
+ }
/**
* Initializes all menu items.
- *
- * @protected
*/
- _initItems: function () {
- elBySelAll('.menuOverlayItemLink', this._menu, this._initItem.bind(this));
- },
+ initItems() {
+ this.menu.querySelectorAll('.menuOverlayItemLink').forEach((element) => {
+ this.initItem(element);
+ });
+ }
/**
* Initializes a single menu item.
- *
- * @param {Element} item menu item
- * @protected
*/
- _initItem: function (item) {
+ initItem(item) {
// check if it should contain a 'more' link w/ an external callback
- var parent = item.parentNode;
- var more = elData(parent, 'more');
+ const parent = item.parentElement;
+ const more = parent.dataset.more;
if (more) {
- item.addEventListener(WCF_CLICK_EVENT, (function (event) {
+ item.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
- EventHandler.fire(this._eventIdentifier, 'more', {
+ EventHandler.fire(this.eventIdentifier, 'more', {
handler: this,
identifier: more,
item: item,
- parent: parent
+ parent: parent,
});
- }).bind(this));
+ });
return;
}
- var itemList = item.nextElementSibling, wrapper;
+ const itemList = item.nextElementSibling;
if (itemList === null) {
return;
}
// handle static items with an icon-type button next to it (acp menu)
if (itemList.nodeName !== 'OL' && itemList.classList.contains('menuOverlayItemLinkIcon')) {
// add wrapper
- wrapper = elCreate('span');
+ const wrapper = document.createElement('span');
wrapper.className = 'menuOverlayItemWrapper';
parent.insertBefore(wrapper, item);
wrapper.appendChild(item);
}
return;
}
- var isLink = (elAttr(item, 'href') !== '#');
- var parentItemList = parent.parentNode;
- var itemTitle = elData(itemList, 'title');
- this._items.set(item, {
+ const isLink = item.href !== '#';
+ const parentItemList = parent.parentElement;
+ let itemTitle = itemList.dataset.title;
+ this.items.set(item, {
itemList: itemList,
- parentItemList: parentItemList
+ parentItemList: parentItemList,
});
- if (itemTitle === '') {
+ if (!itemTitle) {
itemTitle = DomTraverse.childByClass(item, 'menuOverlayItemTitle').textContent;
- elData(itemList, 'title', itemTitle);
+ itemList.dataset.title = itemTitle;
}
- var callbackLink = this._showItemList.bind(this, item);
+ const callbackLink = this.showItemList.bind(this, item);
if (isLink) {
- wrapper = elCreate('span');
+ const wrapper = document.createElement('span');
wrapper.className = 'menuOverlayItemWrapper';
parent.insertBefore(wrapper, item);
wrapper.appendChild(item);
- var moreLink = elCreate('a');
- elAttr(moreLink, 'href', '#');
+ const moreLink = document.createElement('a');
+ moreLink.href = '#';
moreLink.className = 'menuOverlayItemLinkIcon' + (item.classList.contains('active') ? ' active' : '');
moreLink.innerHTML = '<span class="icon icon24 fa-angle-right"></span>';
- moreLink.addEventListener(WCF_CLICK_EVENT, callbackLink);
+ moreLink.addEventListener('click', callbackLink);
wrapper.appendChild(moreLink);
}
else {
item.classList.add('menuOverlayItemLinkMore');
- item.addEventListener(WCF_CLICK_EVENT, callbackLink);
+ item.addEventListener('click', callbackLink);
}
- var backLinkItem = elCreate('li');
+ const backLinkItem = document.createElement('li');
backLinkItem.className = 'menuOverlayHeader';
- wrapper = elCreate('span');
+ const wrapper = document.createElement('span');
wrapper.className = 'menuOverlayItemWrapper';
- var backLink = elCreate('a');
- elAttr(backLink, 'href', '#');
+ const backLink = document.createElement('a');
+ backLink.href = '#';
backLink.className = 'menuOverlayItemLink menuOverlayBackLink';
- backLink.textContent = elData(parentItemList, 'title');
- backLink.addEventListener(WCF_CLICK_EVENT, this._hideItemList.bind(this, item));
- var closeLink = elCreate('a');
- elAttr(closeLink, 'href', '#');
+ backLink.textContent = parentItemList.dataset.title || '';
+ backLink.addEventListener('click', this.hideItemList.bind(this, item));
+ const closeLink = document.createElement('a');
+ closeLink.href = '#';
closeLink.className = 'menuOverlayItemLinkIcon';
closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
- closeLink.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
+ closeLink.addEventListener('click', this.close.bind(this));
wrapper.appendChild(backLink);
wrapper.appendChild(closeLink);
backLinkItem.appendChild(wrapper);
itemList.insertBefore(backLinkItem, itemList.firstElementChild);
if (!backLinkItem.nextElementSibling.classList.contains('menuOverlayTitle')) {
- var titleItem = elCreate('li');
+ const titleItem = document.createElement('li');
titleItem.className = 'menuOverlayTitle';
- var title = elCreate('span');
+ const title = document.createElement('span');
title.textContent = itemTitle;
titleItem.appendChild(title);
itemList.insertBefore(titleItem, backLinkItem.nextElementSibling);
}
- },
+ }
/**
* Renders the menu item list header.
- *
- * @protected
*/
- _initHeader: function () {
- var listItem = elCreate('li');
+ initHeader() {
+ const listItem = document.createElement('li');
listItem.className = 'menuOverlayHeader';
- var wrapper = elCreate('span');
+ const wrapper = document.createElement('span');
wrapper.className = 'menuOverlayItemWrapper';
listItem.appendChild(wrapper);
- var logoWrapper = elCreate('span');
+ const logoWrapper = document.createElement('span');
logoWrapper.className = 'menuOverlayLogoWrapper';
wrapper.appendChild(logoWrapper);
- var logo = elCreate('span');
+ const logo = document.createElement('span');
logo.className = 'menuOverlayLogo';
- logo.style.setProperty('background-image', 'url("' + elData(this._menu, 'page-logo') + '")', '');
+ const pageLogo = this.menu.dataset.pageLogo;
+ logo.style.setProperty('background-image', `url("${pageLogo}")`, '');
logoWrapper.appendChild(logo);
- var closeLink = elCreate('a');
- elAttr(closeLink, 'href', '#');
+ const closeLink = document.createElement('a');
+ closeLink.href = '#';
closeLink.className = 'menuOverlayItemLinkIcon';
closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
- closeLink.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
+ closeLink.addEventListener('click', this.close.bind(this));
wrapper.appendChild(closeLink);
- var list = DomTraverse.childByClass(this._menu, 'menuOverlayItemList');
+ const list = DomTraverse.childByClass(this.menu, 'menuOverlayItemList');
list.insertBefore(listItem, list.firstElementChild);
- },
+ }
/**
* Hides an item list, return to the parent item list.
- *
- * @param {Element} item menu item
- * @param {Event} event event object
- * @protected
*/
- _hideItemList: function (item, event) {
+ hideItemList(item, event) {
if (event instanceof Event) {
event.preventDefault();
}
- this._menu.classList.remove('allowScroll');
- this._removeActiveList = true;
- var data = this._items.get(item);
+ this.menu.classList.remove('allowScroll');
+ this.removeActiveList = true;
+ const data = this.items.get(item);
data.parentItemList.classList.remove('hidden');
- this._updateDepth(false);
- },
+ this.updateDepth(false);
+ }
/**
* Shows the child item list.
- *
- * @param {Element} item menu item
- * @param event
- * @private
*/
- _showItemList: function (item, event) {
- if (event instanceof Event) {
- event.preventDefault();
- }
- var data = this._items.get(item);
- var load = elData(data.itemList, 'load');
+ showItemList(item, event) {
+ event.preventDefault();
+ const data = this.items.get(item);
+ const load = data.itemList.dataset.load;
if (load) {
- if (!elDataBool(item, 'loaded')) {
- var icon = event.currentTarget.firstElementChild;
+ if (!Core.stringToBool(item.dataset.loaded || '')) {
+ const target = event.currentTarget;
+ const icon = target.firstElementChild;
if (icon.classList.contains('fa-angle-right')) {
icon.classList.remove('fa-angle-right');
icon.classList.add('fa-spinner');
}
- EventHandler.fire(this._eventIdentifier, 'load_' + load);
+ EventHandler.fire(this.eventIdentifier, 'load_' + load);
return;
}
}
- this._menu.classList.remove('allowScroll');
+ this.menu.classList.remove('allowScroll');
data.itemList.classList.add('activeList');
data.parentItemList.classList.add('hidden');
- this._activeList.push(data.itemList);
- this._updateDepth(true);
- },
- _updateDepth: function (increase) {
- this._depth += (increase) ? 1 : -1;
- var offset = this._depth * -100;
+ this.activeList.push(data.itemList);
+ this.updateDepth(true);
+ }
+ updateDepth(increase) {
+ this.depth += (increase) ? 1 : -1;
+ let offset = this.depth * -100;
if (Language.get('wcf.global.pageDirection') === 'rtl') {
// reverse logic for RTL
offset *= -1;
}
- this._menu.children[0].style.setProperty('transform', 'translateX(' + offset + '%)', '');
- },
- _updateButtonState: function () {
- var hasNewContent = false;
- var itemList = elBySel('.menuOverlayItemList', this._menu);
- elBySelAll('.badgeUpdate', this._menu, function (badge) {
- if (~~badge.textContent > 0 && badge.closest('.menuOverlayItemList') === itemList) {
+ const child = this.menu.children[0];
+ child.style.setProperty('transform', `translateX(${offset}%)`, '');
+ }
+ updateButtonState() {
+ let hasNewContent = false;
+ const itemList = this.menu.querySelector('.menuOverlayItemList');
+ this.menu.querySelectorAll('.badgeUpdate').forEach(badge => {
+ const value = badge.textContent;
+ if (~~value > 0 && badge.closest('.menuOverlayItemList') === itemList) {
hasNewContent = true;
}
});
- this._button.classList[(hasNewContent ? 'add' : 'remove')]('pageMenuMobileButtonHasContent');
+ this.button.classList[hasNewContent ? 'add' : 'remove']('pageMenuMobileButtonHasContent');
}
- };
+ }
return UiPageMenuAbstract;
});
/**
* Triggers a custom or built-in event.
*/
-export function triggerEvent(element: Element, eventName: string): void {
+export function triggerEvent(element: EventTarget, eventName: string): void {
const event = new Event(eventName, {
bubbles: true,
cancelable: true,
/**
* Prepends an element to a parent element.
*
- * @deprecated 5.3 Use `parent.insertBefore(element, parent.firstChild)` instead.
+ * @deprecated 5.3 Use `parent.insertAdjacentElement('afterbegin', element)` instead.
*/
prepend(element: Element, parent: Element): void {
- parent.insertBefore(element, parent.firstChild);
+ parent.insertAdjacentElement('afterbegin', element);
},
/**
* Inserts an element after an existing element.
*
- * @deprecated 5.3 Use `element.parentNode.insertBefore(newElement, element.nextSibling)` instead.
+ * @deprecated 5.3 Use `element.insertAdjacentElement('afterend', newElement)` instead.
*/
insertAfter(newElement: Element, element: Element): void {
- if (element.parentNode === null) {
- throw new Error('The target element has no parent.');
- }
-
- element.parentNode.insertBefore(newElement, element.nextSibling);
+ element.insertAdjacentElement('afterend', newElement);
},
/**
+++ /dev/null
-/**
- * Provides a touch-friendly fullscreen 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/Menu/Abstract
- */
-define(['Core', 'Environment', 'EventHandler', 'Language', 'ObjectMap', 'Dom/Traverse', 'Dom/Util', 'Ui/Screen'], function(Core, Environment, EventHandler, Language, ObjectMap, DomTraverse, DomUtil, UiScreen) {
- "use strict";
-
- var _pageContainer = elById('pageContainer');
-
- /**
- * Which edge of the menu is touched? Empty string
- * if no menu is currently touched.
- *
- * One 'left', 'right' or ''.
- */
- var _androidTouching = '';
-
- /**
- * @param {string} eventIdentifier event namespace
- * @param {string} elementId menu element id
- * @param {string} buttonSelector CSS selector for toggle button
- * @constructor
- */
- function UiPageMenuAbstract(eventIdentifier, elementId, buttonSelector) { this.init(eventIdentifier, elementId, buttonSelector); }
- UiPageMenuAbstract.prototype = {
- /**
- * Initializes a touch-friendly fullscreen menu.
- *
- * @param {string} eventIdentifier event namespace
- * @param {string} elementId menu element id
- * @param {string} buttonSelector CSS selector for toggle button
- */
- init: function(eventIdentifier, elementId, buttonSelector) {
- if (elData(document.body, 'template') === 'packageInstallationSetup') {
- // work-around for WCFSetup on mobile
- return;
- }
-
- this._activeList = [];
- this._depth = 0;
- this._enabled = true;
- this._eventIdentifier = eventIdentifier;
- this._items = new ObjectMap();
- this._menu = elById(elementId);
- this._removeActiveList = false;
-
- var callbackOpen = this.open.bind(this);
- this._button = elBySel(buttonSelector);
- this._button.addEventListener(WCF_CLICK_EVENT, callbackOpen);
-
- this._initItems();
- this._initHeader();
-
- EventHandler.add(this._eventIdentifier, 'open', callbackOpen);
- EventHandler.add(this._eventIdentifier, 'close', this.close.bind(this));
- EventHandler.add(this._eventIdentifier, 'updateButtonState', this._updateButtonState.bind(this));
-
- var itemList, itemLists = elByClass('menuOverlayItemList', this._menu);
- this._menu.addEventListener('animationend', (function() {
- if (!this._menu.classList.contains('open')) {
- for (var i = 0, length = itemLists.length; i < length; i++) {
- itemList = itemLists[i];
-
- // force the main list to be displayed
- itemList.classList.remove('active');
- itemList.classList.remove('hidden');
- }
- }
- }).bind(this));
-
- this._menu.children[0].addEventListener('transitionend', (function() {
- this._menu.classList.add('allowScroll');
-
- if (this._removeActiveList) {
- this._removeActiveList = false;
-
- var list = this._activeList.pop();
- if (list) {
- list.classList.remove('activeList');
- }
- }
- }).bind(this));
-
- var backdrop = elCreate('div');
- backdrop.className = 'menuOverlayMobileBackdrop';
- backdrop.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
-
- DomUtil.insertAfter(backdrop, this._menu);
-
- this._updateButtonState();
-
- if (Environment.platform() === 'android') {
- this._initializeAndroid();
- }
- },
-
- /**
- * Opens the menu.
- *
- * @param {Event} event event object
- * @return {boolean} true if menu has been opened
- */
- open: function(event) {
- if (!this._enabled) {
- return false;
- }
-
- if (event instanceof Event) {
- event.preventDefault();
- }
-
- this._menu.classList.add('open');
- this._menu.classList.add('allowScroll');
- this._menu.children[0].classList.add('activeList');
-
- UiScreen.scrollDisable();
-
- _pageContainer.classList.add('menuOverlay-' + this._menu.id);
-
- UiScreen.pageOverlayOpen();
-
- return true;
- },
-
- /**
- * Closes the menu.
- *
- * @param {(Event|boolean)} event event object or boolean true to force close the menu
- * @return {boolean} true if menu was open
- */
- close: function(event) {
- if (event instanceof Event) {
- event.preventDefault();
- }
-
- if (this._menu.classList.contains('open')) {
- this._menu.classList.remove('open');
-
- UiScreen.scrollEnable();
- UiScreen.pageOverlayClose();
-
- _pageContainer.classList.remove('menuOverlay-' + this._menu.id);
-
- return true;
- }
-
- return false;
- },
-
- /**
- * Enables the touch menu.
- */
- enable: function() {
- this._enabled = true;
- },
-
- /**
- * Disables the touch menu.
- */
- disable: function() {
- this._enabled = false;
-
- this.close(true);
- },
-
- /**
- * Initializes the Android Touch Menu.
- */
- _initializeAndroid: function() {
- var appearsAt, backdrop, touchStart;
- /** @const */ var AT_EDGE = 20;
- /** @const */ var MOVED_HORIZONTALLY = 5;
- /** @const */ var MOVED_VERTICALLY = 20;
-
- // specify on which side of the page the menu appears
- switch (this._menu.id) {
- case 'pageUserMenuMobile':
- appearsAt = 'right';
- break;
- case 'pageMainMenuMobile':
- appearsAt = 'left';
- break;
- default:
- return;
- }
-
- backdrop = this._menu.nextElementSibling;
-
- // horizontal position of the touch start
- touchStart = null;
-
- document.addEventListener('touchstart', (function(event) {
- var touches, isOpen, isLeftEdge, isRightEdge;
- touches = event.touches;
-
- isOpen = this._menu.classList.contains('open');
-
- // check whether we touch the edges of the menu
- if (appearsAt === 'left') {
- isLeftEdge = !isOpen && (touches[0].clientX < AT_EDGE);
- isRightEdge = isOpen && (Math.abs(this._menu.offsetWidth - touches[0].clientX) < AT_EDGE);
- }
- else if (appearsAt === 'right') {
- isLeftEdge = isOpen && (Math.abs(document.body.clientWidth - this._menu.offsetWidth - touches[0].clientX) < AT_EDGE);
- isRightEdge = !isOpen && ((document.body.clientWidth - touches[0].clientX) < AT_EDGE);
- }
-
- // abort if more than one touch
- if (touches.length > 1) {
- if (_androidTouching) {
- Core.triggerEvent(document, 'touchend');
- }
- return;
- }
-
- // break if a touch is in progress
- if (_androidTouching) return;
- // break if no edge has been touched
- if (!isLeftEdge && !isRightEdge) return;
- // break if a different menu is open
- if (UiScreen.pageOverlayIsActive()) {
- var found = false;
- for (var i = 0; i < _pageContainer.classList.length; i++) {
- if (_pageContainer.classList[i] === 'menuOverlay-' + this._menu.id) {
- found = true;
- }
- }
- if (!found) return;
- }
- // break if redactor is in use
- if (document.documentElement.classList.contains('redactorActive')) return;
-
- touchStart = {
- x: touches[0].clientX,
- y: touches[0].clientY
- };
-
- if (isLeftEdge) _androidTouching = 'left';
- if (isRightEdge) _androidTouching = 'right';
- }).bind(this));
-
- document.addEventListener('touchend', (function(event) {
- // break if we did not start a touch
- if (!_androidTouching || touchStart === null) return;
-
- // break if the menu did not even start opening
- if (!this._menu.classList.contains('open')) {
- // reset
- touchStart = null;
- _androidTouching = '';
- return;
- }
-
- // last known position of the finger
- var position;
- if (event) {
- position = event.changedTouches[0].clientX;
- }
- else {
- position = touchStart.x;
- }
-
- // clean up touch styles
- this._menu.classList.add('androidMenuTouchEnd');
- this._menu.style.removeProperty('transform');
- backdrop.style.removeProperty(appearsAt);
- this._menu.addEventListener('transitionend', (function() {
- this._menu.classList.remove('androidMenuTouchEnd');
- }).bind(this), { once: true });
-
- // check whether the user moved the finger far enough
- if (appearsAt === 'left') {
- if (_androidTouching === 'left' && position < (touchStart.x + 100)) this.close();
- if (_androidTouching === 'right' && position < (touchStart.x - 100)) this.close();
- }
- else if (appearsAt === 'right') {
- if (_androidTouching === 'left' && position > (touchStart.x + 100)) this.close();
- if (_androidTouching === 'right' && position > (touchStart.x - 100)) this.close();
- }
-
- // reset
- touchStart = null;
- _androidTouching = '';
- }).bind(this));
-
- document.addEventListener('touchmove', (function(event) {
- // break if we did not start a touch
- if (!_androidTouching || touchStart === null) return;
-
- var touches = event.touches;
-
- // check whether the user started moving in the correct direction
- // this avoids false positives, in case the user just wanted to tap
- var movedFromEdge = false, movedVertically = false;
- if (_androidTouching === 'left') movedFromEdge = touches[0].clientX > (touchStart.x + MOVED_HORIZONTALLY);
- if (_androidTouching === 'right') movedFromEdge = touches[0].clientX < (touchStart.x - MOVED_HORIZONTALLY);
- movedVertically = Math.abs(touches[0].clientY - touchStart.y) > MOVED_VERTICALLY;
-
- var isOpen = this._menu.classList.contains('open');
-
- if (!isOpen && movedFromEdge && !movedVertically) {
- // the menu is not yet open, but the user moved into the right direction
- this.open();
- isOpen = true;
- }
-
- if (isOpen) {
- // update CSS to the new finger position
- var position = touches[0].clientX;
- if (appearsAt === 'right') position = document.body.clientWidth - position;
- if (position > this._menu.offsetWidth) position = this._menu.offsetWidth;
- if (position < 0) position = 0;
- this._menu.style.setProperty('transform', 'translateX(' + (appearsAt === 'left' ? 1 : -1) * (position - this._menu.offsetWidth) + 'px)');
- backdrop.style.setProperty(appearsAt, Math.min(this._menu.offsetWidth, position) + 'px');
- }
- }).bind(this));
- },
-
- /**
- * Initializes all menu items.
- *
- * @protected
- */
- _initItems: function() {
- elBySelAll('.menuOverlayItemLink', this._menu, this._initItem.bind(this));
- },
-
- /**
- * Initializes a single menu item.
- *
- * @param {Element} item menu item
- * @protected
- */
- _initItem: function(item) {
- // check if it should contain a 'more' link w/ an external callback
- var parent = item.parentNode;
- var more = elData(parent, 'more');
- if (more) {
- item.addEventListener(WCF_CLICK_EVENT, (function(event) {
- event.preventDefault();
- event.stopPropagation();
-
- EventHandler.fire(this._eventIdentifier, 'more', {
- handler: this,
- identifier: more,
- item: item,
- parent: parent
- });
- }).bind(this));
-
- return;
- }
-
- var itemList = item.nextElementSibling, wrapper;
- if (itemList === null) {
- return;
- }
-
- // handle static items with an icon-type button next to it (acp menu)
- if (itemList.nodeName !== 'OL' && itemList.classList.contains('menuOverlayItemLinkIcon')) {
- // add wrapper
- wrapper = elCreate('span');
- wrapper.className = 'menuOverlayItemWrapper';
- parent.insertBefore(wrapper, item);
- wrapper.appendChild(item);
-
- while (wrapper.nextElementSibling) {
- wrapper.appendChild(wrapper.nextElementSibling);
- }
-
- return;
- }
-
- var isLink = (elAttr(item, 'href') !== '#');
- var parentItemList = parent.parentNode;
- var itemTitle = elData(itemList, 'title');
-
- this._items.set(item, {
- itemList: itemList,
- parentItemList: parentItemList
- });
-
- if (itemTitle === '') {
- itemTitle = DomTraverse.childByClass(item, 'menuOverlayItemTitle').textContent;
- elData(itemList, 'title', itemTitle);
- }
-
- var callbackLink = this._showItemList.bind(this, item);
- if (isLink) {
- wrapper = elCreate('span');
- wrapper.className = 'menuOverlayItemWrapper';
- parent.insertBefore(wrapper, item);
- wrapper.appendChild(item);
-
- var moreLink = elCreate('a');
- elAttr(moreLink, 'href', '#');
- moreLink.className = 'menuOverlayItemLinkIcon' + (item.classList.contains('active') ? ' active' : '');
- moreLink.innerHTML = '<span class="icon icon24 fa-angle-right"></span>';
- moreLink.addEventListener(WCF_CLICK_EVENT, callbackLink);
- wrapper.appendChild(moreLink);
- }
- else {
- item.classList.add('menuOverlayItemLinkMore');
- item.addEventListener(WCF_CLICK_EVENT, callbackLink);
- }
-
- var backLinkItem = elCreate('li');
- backLinkItem.className = 'menuOverlayHeader';
-
- wrapper = elCreate('span');
- wrapper.className = 'menuOverlayItemWrapper';
-
- var backLink = elCreate('a');
- elAttr(backLink, 'href', '#');
- backLink.className = 'menuOverlayItemLink menuOverlayBackLink';
- backLink.textContent = elData(parentItemList, 'title');
- backLink.addEventListener(WCF_CLICK_EVENT, this._hideItemList.bind(this, item));
-
- var closeLink = elCreate('a');
- elAttr(closeLink, 'href', '#');
- closeLink.className = 'menuOverlayItemLinkIcon';
- closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
- closeLink.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
-
- wrapper.appendChild(backLink);
- wrapper.appendChild(closeLink);
- backLinkItem.appendChild(wrapper);
-
- itemList.insertBefore(backLinkItem, itemList.firstElementChild);
-
- if (!backLinkItem.nextElementSibling.classList.contains('menuOverlayTitle')) {
- var titleItem = elCreate('li');
- titleItem.className = 'menuOverlayTitle';
- var title = elCreate('span');
- title.textContent = itemTitle;
- titleItem.appendChild(title);
-
- itemList.insertBefore(titleItem, backLinkItem.nextElementSibling);
- }
- },
-
- /**
- * Renders the menu item list header.
- *
- * @protected
- */
- _initHeader: function() {
- var listItem = elCreate('li');
- listItem.className = 'menuOverlayHeader';
-
- var wrapper = elCreate('span');
- wrapper.className = 'menuOverlayItemWrapper';
- listItem.appendChild(wrapper);
-
- var logoWrapper = elCreate('span');
- logoWrapper.className = 'menuOverlayLogoWrapper';
- wrapper.appendChild(logoWrapper);
-
- var logo = elCreate('span');
- logo.className = 'menuOverlayLogo';
- logo.style.setProperty('background-image', 'url("' + elData(this._menu, 'page-logo') + '")', '');
- logoWrapper.appendChild(logo);
-
- var closeLink = elCreate('a');
- elAttr(closeLink, 'href', '#');
- closeLink.className = 'menuOverlayItemLinkIcon';
- closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
- closeLink.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
- wrapper.appendChild(closeLink);
-
- var list = DomTraverse.childByClass(this._menu, 'menuOverlayItemList');
- list.insertBefore(listItem, list.firstElementChild);
- },
-
- /**
- * Hides an item list, return to the parent item list.
- *
- * @param {Element} item menu item
- * @param {Event} event event object
- * @protected
- */
- _hideItemList: function(item, event) {
- if (event instanceof Event) {
- event.preventDefault();
- }
-
- this._menu.classList.remove('allowScroll');
- this._removeActiveList = true;
-
- var data = this._items.get(item);
- data.parentItemList.classList.remove('hidden');
-
- this._updateDepth(false);
- },
-
- /**
- * Shows the child item list.
- *
- * @param {Element} item menu item
- * @param event
- * @private
- */
- _showItemList: function(item, event) {
- if (event instanceof Event) {
- event.preventDefault();
- }
-
- var data = this._items.get(item);
-
- var load = elData(data.itemList, 'load');
- if (load) {
- if (!elDataBool(item, 'loaded')) {
- var icon = event.currentTarget.firstElementChild;
- if (icon.classList.contains('fa-angle-right')) {
- icon.classList.remove('fa-angle-right');
- icon.classList.add('fa-spinner');
- }
-
- EventHandler.fire(this._eventIdentifier, 'load_' + load);
-
- return;
- }
- }
-
- this._menu.classList.remove('allowScroll');
-
- data.itemList.classList.add('activeList');
- data.parentItemList.classList.add('hidden');
-
- this._activeList.push(data.itemList);
-
- this._updateDepth(true);
- },
-
- _updateDepth: function(increase) {
- this._depth += (increase) ? 1 : -1;
-
- var offset = this._depth * -100;
- if (Language.get('wcf.global.pageDirection') === 'rtl') {
- // reverse logic for RTL
- offset *= -1;
- }
-
- this._menu.children[0].style.setProperty('transform', 'translateX(' + offset + '%)', '');
- },
-
- _updateButtonState: function() {
- var hasNewContent = false;
- var itemList = elBySel('.menuOverlayItemList', this._menu);
- elBySelAll('.badgeUpdate', this._menu, function (badge) {
- if (~~badge.textContent > 0 && badge.closest('.menuOverlayItemList') === itemList) {
- hasNewContent = true;
- }
- });
-
- this._button.classList[(hasNewContent ? 'add' : 'remove')]('pageMenuMobileButtonHasContent');
- }
- };
-
- return UiPageMenuAbstract;
-});
--- /dev/null
+/**
+ * Provides a touch-friendly fullscreen 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/Menu/Abstract
+ */
+
+import * as Core from '../../../Core';
+import * as Environment from '../../../Environment';
+import * as EventHandler from '../../../Event/Handler';
+import * as Language from '../../../Language';
+import * as DomTraverse from '../../../Dom/Traverse';
+import * as UiScreen from '../../Screen';
+
+const _pageContainer = document.getElementById('pageContainer')!;
+
+const enum TouchPosition {
+ AtEdge = 20,
+ MovedHorizontally = 5,
+ MovedVertically = 20,
+}
+
+/**
+ * Which edge of the menu is touched? Empty string
+ * if no menu is currently touched.
+ *
+ * One 'left', 'right' or ''.
+ */
+let _androidTouching = '';
+
+interface ItemData {
+ itemList: HTMLOListElement;
+ parentItemList: HTMLOListElement;
+}
+
+abstract class UiPageMenuAbstract {
+ private readonly activeList: HTMLOListElement[] = [];
+ private readonly button: HTMLElement;
+ private depth = 0;
+ private enabled: boolean = true;
+ private readonly eventIdentifier: string;
+ private readonly items = new Map<HTMLAnchorElement, ItemData>();
+ private readonly menu: HTMLElement;
+ private removeActiveList = false;
+
+ protected constructor(eventIdentifier: string, elementId: string, buttonSelector: string) {
+ if (document.body.dataset.template === 'packageInstallationSetup') {
+ // work-around for WCFSetup on mobile
+ return;
+ }
+
+ this.eventIdentifier = eventIdentifier;
+ this.menu = document.getElementById(elementId)!;
+
+ const callbackOpen = this.open.bind(this);
+ this.button = document.querySelector(buttonSelector) as HTMLElement;
+ this.button.addEventListener('click', callbackOpen);
+
+ this.initItems();
+ this.initHeader();
+
+ EventHandler.add(this.eventIdentifier, 'open', callbackOpen);
+ EventHandler.add(this.eventIdentifier, 'close', this.close.bind(this));
+ EventHandler.add(this.eventIdentifier, 'updateButtonState', this.updateButtonState.bind(this));
+
+ this.menu.addEventListener('animationend', () => {
+ if (!this.menu.classList.contains('open')) {
+ this.menu.querySelectorAll('.menuOverlayItemList').forEach(itemList => {
+ // force the main list to be displayed
+ itemList.classList.remove('active', 'hidden');
+ });
+ }
+ });
+
+ this.menu.children[0].addEventListener('transitionend', () => {
+ this.menu.classList.add('allowScroll');
+
+ if (this.removeActiveList) {
+ this.removeActiveList = false;
+
+ const list = this.activeList.pop();
+ if (list) {
+ list.classList.remove('activeList');
+ }
+ }
+ });
+
+ const backdrop = document.createElement('div');
+ backdrop.className = 'menuOverlayMobileBackdrop';
+ backdrop.addEventListener('click', this.close.bind(this));
+
+ this.menu.insertAdjacentElement('afterend', backdrop);
+
+ this.menu.parentElement!.insertBefore(backdrop, this.menu.nextSibling);
+
+ this.updateButtonState();
+
+ if (Environment.platform() === 'android') {
+ this.initializeAndroid();
+ }
+ }
+
+ /**
+ * Opens the menu.
+ */
+ open(event?: MouseEvent): boolean {
+ if (!this.enabled) {
+ return false;
+ }
+
+ if (event instanceof Event) {
+ event.preventDefault();
+ }
+
+ this.menu.classList.add('open');
+ this.menu.classList.add('allowScroll');
+ this.menu.children[0].classList.add('activeList');
+
+ UiScreen.scrollDisable();
+
+ _pageContainer.classList.add('menuOverlay-' + this.menu.id);
+
+ UiScreen.pageOverlayOpen();
+
+ return true;
+ }
+
+ /**
+ * Closes the menu.
+ */
+ close(event?: Event): boolean {
+ if (event instanceof Event) {
+ event.preventDefault();
+ }
+
+ if (this.menu.classList.contains('open')) {
+ this.menu.classList.remove('open');
+
+ UiScreen.scrollEnable();
+ UiScreen.pageOverlayClose();
+
+ _pageContainer.classList.remove('menuOverlay-' + this.menu.id);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Enables the touch menu.
+ */
+ enable(): void {
+ this.enabled = true;
+ }
+
+ /**
+ * Disables the touch menu.
+ */
+ disable(): void {
+ this.enabled = false;
+
+ this.close();
+ }
+
+ /**
+ * Initializes the Android Touch Menu.
+ */
+ private initializeAndroid(): void {
+ // specify on which side of the page the menu appears
+ let appearsAt: "left" | "right";
+ switch (this.menu.id) {
+ case 'pageUserMenuMobile':
+ appearsAt = 'right';
+ break;
+ case 'pageMainMenuMobile':
+ appearsAt = 'left';
+ break;
+ default:
+ return;
+ }
+
+ const backdrop = this.menu.nextElementSibling as HTMLElement;
+
+ // horizontal position of the touch start
+ let touchStart: { x: number; y: number } | undefined = undefined;
+
+ document.addEventListener('touchstart', event => {
+ const touches = event.touches;
+
+ let isLeftEdge: boolean;
+ let isRightEdge: boolean;
+
+ const isOpen = this.menu.classList.contains('open');
+
+ // check whether we touch the edges of the menu
+ if (appearsAt === 'left') {
+ isLeftEdge = !isOpen && (touches[0].clientX < TouchPosition.AtEdge);
+ isRightEdge = isOpen && (Math.abs(this.menu.offsetWidth - touches[0].clientX) < TouchPosition.AtEdge);
+ } else {
+ isLeftEdge = isOpen && (Math.abs(document.body.clientWidth - this.menu.offsetWidth - touches[0].clientX) < TouchPosition.AtEdge);
+ isRightEdge = !isOpen && ((document.body.clientWidth - touches[0].clientX) < TouchPosition.AtEdge);
+ }
+
+ // abort if more than one touch
+ if (touches.length > 1) {
+ if (_androidTouching) {
+ Core.triggerEvent(document, 'touchend');
+ }
+ return;
+ }
+
+ // break if a touch is in progress
+ if (_androidTouching) {
+ return;
+ }
+
+ // break if no edge has been touched
+ if (!isLeftEdge && !isRightEdge) {
+ return;
+ }
+
+ // break if a different menu is open
+ if (UiScreen.pageOverlayIsActive()) {
+ const found = _pageContainer.classList.contains(`menuOverlay-${this.menu.id}`);
+ if (!found) {
+ return;
+ }
+ }
+ // break if redactor is in use
+ if (document.documentElement.classList.contains('redactorActive')) {
+ return;
+ }
+
+ touchStart = {
+ x: touches[0].clientX,
+ y: touches[0].clientY,
+ };
+
+ if (isLeftEdge) _androidTouching = 'left';
+ if (isRightEdge) _androidTouching = 'right';
+ });
+
+ document.addEventListener('touchend', event => {
+ // break if we did not start a touch
+ if (!_androidTouching || !touchStart) {
+ return;
+ }
+
+ // break if the menu did not even start opening
+ if (!this.menu.classList.contains('open')) {
+ // reset
+ touchStart = undefined;
+ _androidTouching = '';
+ return;
+ }
+
+ // last known position of the finger
+ let position: number;
+ if (event) {
+ position = event.changedTouches[0].clientX;
+ } else {
+ position = touchStart.x;
+ }
+
+ // clean up touch styles
+ this.menu.classList.add('androidMenuTouchEnd');
+ this.menu.style.removeProperty('transform');
+ backdrop.style.removeProperty(appearsAt);
+ this.menu.addEventListener('transitionend', () => {
+ this.menu.classList.remove('androidMenuTouchEnd');
+ }, {once: true});
+
+ // check whether the user moved the finger far enough
+ if (appearsAt === 'left') {
+ if (_androidTouching === 'left' && position < (touchStart.x + 100)) this.close();
+ if (_androidTouching === 'right' && position < (touchStart.x - 100)) this.close();
+ } else {
+ if (_androidTouching === 'left' && position > (touchStart.x + 100)) this.close();
+ if (_androidTouching === 'right' && position > (touchStart.x - 100)) this.close();
+ }
+
+ // reset
+ touchStart = undefined;
+ _androidTouching = '';
+ });
+
+ document.addEventListener('touchmove', event => {
+ // break if we did not start a touch
+ if (!_androidTouching || !touchStart) {
+ return;
+ }
+
+ const touches = event.touches;
+
+ // check whether the user started moving in the correct direction
+ // this avoids false positives, in case the user just wanted to tap
+ let movedFromEdge = false;
+ if (_androidTouching === 'left') movedFromEdge = touches[0].clientX > (touchStart.x + TouchPosition.MovedHorizontally);
+ if (_androidTouching === 'right') movedFromEdge = touches[0].clientX < (touchStart.x - TouchPosition.MovedHorizontally);
+
+ const movedVertically = Math.abs(touches[0].clientY - touchStart.y) > TouchPosition.MovedVertically;
+
+ let isOpen = this.menu.classList.contains('open');
+ if (!isOpen && movedFromEdge && !movedVertically) {
+ // the menu is not yet open, but the user moved into the right direction
+ this.open();
+ isOpen = true;
+ }
+
+ if (isOpen) {
+ // update CSS to the new finger position
+ let position = touches[0].clientX;
+ if (appearsAt === 'right') position = document.body.clientWidth - position;
+ if (position > this.menu.offsetWidth) position = this.menu.offsetWidth;
+ if (position < 0) position = 0;
+ this.menu.style.setProperty('transform', 'translateX(' + (appearsAt === 'left' ? 1 : -1) * (position - this.menu.offsetWidth) + 'px)');
+ backdrop.style.setProperty(appearsAt, Math.min(this.menu.offsetWidth, position) + 'px');
+ }
+ });
+ }
+
+ /**
+ * Initializes all menu items.
+ */
+ private initItems(): void {
+ this.menu.querySelectorAll('.menuOverlayItemLink').forEach((element: HTMLAnchorElement) => {
+ this.initItem(element);
+ });
+ }
+
+ /**
+ * Initializes a single menu item.
+ */
+ private initItem(item: HTMLAnchorElement): void {
+ // check if it should contain a 'more' link w/ an external callback
+ const parent = item.parentElement!;
+ const more = parent.dataset.more;
+ if (more) {
+ item.addEventListener('click', event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ EventHandler.fire(this.eventIdentifier, 'more', {
+ handler: this,
+ identifier: more,
+ item: item,
+ parent: parent,
+ });
+ });
+
+ return;
+ }
+
+ const itemList = item.nextElementSibling as HTMLOListElement;
+ if (itemList === null) {
+ return;
+ }
+
+ // handle static items with an icon-type button next to it (acp menu)
+ if (itemList.nodeName !== 'OL' && itemList.classList.contains('menuOverlayItemLinkIcon')) {
+ // add wrapper
+ const wrapper = document.createElement('span');
+ wrapper.className = 'menuOverlayItemWrapper';
+ parent.insertBefore(wrapper, item);
+ wrapper.appendChild(item);
+
+ while (wrapper.nextElementSibling) {
+ wrapper.appendChild(wrapper.nextElementSibling);
+ }
+
+ return;
+ }
+
+ const isLink = item.href !== '#';
+ const parentItemList = parent.parentElement as HTMLOListElement;
+ let itemTitle = itemList.dataset.title;
+
+ this.items.set(item, {
+ itemList: itemList,
+ parentItemList: parentItemList,
+ });
+
+ if (!itemTitle) {
+ itemTitle = DomTraverse.childByClass(item, 'menuOverlayItemTitle')!.textContent!;
+ itemList.dataset.title = itemTitle;
+ }
+
+ const callbackLink = this.showItemList.bind(this, item);
+ if (isLink) {
+ const wrapper = document.createElement('span');
+ wrapper.className = 'menuOverlayItemWrapper';
+ parent.insertBefore(wrapper, item);
+ wrapper.appendChild(item);
+
+ const moreLink = document.createElement('a');
+ moreLink.href = '#';
+ moreLink.className = 'menuOverlayItemLinkIcon' + (item.classList.contains('active') ? ' active' : '');
+ moreLink.innerHTML = '<span class="icon icon24 fa-angle-right"></span>';
+ moreLink.addEventListener('click', callbackLink);
+ wrapper.appendChild(moreLink);
+ } else {
+ item.classList.add('menuOverlayItemLinkMore');
+ item.addEventListener('click', callbackLink);
+ }
+
+ const backLinkItem = document.createElement('li');
+ backLinkItem.className = 'menuOverlayHeader';
+
+ const wrapper = document.createElement('span');
+ wrapper.className = 'menuOverlayItemWrapper';
+
+ const backLink = document.createElement('a');
+ backLink.href = '#';
+ backLink.className = 'menuOverlayItemLink menuOverlayBackLink';
+ backLink.textContent = parentItemList.dataset.title || '';
+ backLink.addEventListener('click', this.hideItemList.bind(this, item));
+
+ const closeLink = document.createElement('a');
+ closeLink.href = '#';
+ closeLink.className = 'menuOverlayItemLinkIcon';
+ closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
+ closeLink.addEventListener('click', this.close.bind(this));
+
+ wrapper.appendChild(backLink);
+ wrapper.appendChild(closeLink);
+ backLinkItem.appendChild(wrapper);
+
+ itemList.insertBefore(backLinkItem, itemList.firstElementChild);
+
+ if (!backLinkItem.nextElementSibling!.classList.contains('menuOverlayTitle')) {
+ const titleItem = document.createElement('li');
+ titleItem.className = 'menuOverlayTitle';
+ const title = document.createElement('span');
+ title.textContent = itemTitle;
+ titleItem.appendChild(title);
+
+ itemList.insertBefore(titleItem, backLinkItem.nextElementSibling);
+ }
+ }
+
+ /**
+ * Renders the menu item list header.
+ */
+ private initHeader(): void {
+ const listItem = document.createElement('li');
+ listItem.className = 'menuOverlayHeader';
+
+ const wrapper = document.createElement('span');
+ wrapper.className = 'menuOverlayItemWrapper';
+ listItem.appendChild(wrapper);
+
+ const logoWrapper = document.createElement('span');
+ logoWrapper.className = 'menuOverlayLogoWrapper';
+ wrapper.appendChild(logoWrapper);
+
+ const logo = document.createElement('span');
+ logo.className = 'menuOverlayLogo';
+ const pageLogo = this.menu.dataset.pageLogo!;
+ logo.style.setProperty('background-image', `url("${pageLogo}")`, '');
+ logoWrapper.appendChild(logo);
+
+ const closeLink = document.createElement('a');
+ closeLink.href = '#';
+ closeLink.className = 'menuOverlayItemLinkIcon';
+ closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
+ closeLink.addEventListener('click', this.close.bind(this));
+ wrapper.appendChild(closeLink);
+
+ const list = DomTraverse.childByClass(this.menu, 'menuOverlayItemList')!;
+ list.insertBefore(listItem, list.firstElementChild);
+ }
+
+ /**
+ * Hides an item list, return to the parent item list.
+ */
+ private hideItemList(item: HTMLAnchorElement, event: MouseEvent): void {
+ if (event instanceof Event) {
+ event.preventDefault();
+ }
+
+ this.menu.classList.remove('allowScroll');
+ this.removeActiveList = true;
+
+ const data = this.items.get(item)!;
+ data.parentItemList.classList.remove('hidden');
+
+ this.updateDepth(false);
+ }
+
+ /**
+ * Shows the child item list.
+ */
+ private showItemList(item: HTMLAnchorElement, event: MouseEvent): void {
+ event.preventDefault();
+
+ const data = this.items.get(item)!;
+
+ const load = data.itemList.dataset.load;
+ if (load) {
+ if (!Core.stringToBool(item.dataset.loaded || '')) {
+ const target = event.currentTarget as HTMLElement;
+ const icon = target.firstElementChild!;
+ if (icon.classList.contains('fa-angle-right')) {
+ icon.classList.remove('fa-angle-right');
+ icon.classList.add('fa-spinner');
+ }
+
+ EventHandler.fire(this.eventIdentifier, 'load_' + load);
+
+ return;
+ }
+ }
+
+ this.menu.classList.remove('allowScroll');
+
+ data.itemList.classList.add('activeList');
+ data.parentItemList.classList.add('hidden');
+
+ this.activeList.push(data.itemList);
+
+ this.updateDepth(true);
+ }
+
+ private updateDepth(increase: boolean): void {
+ this.depth += (increase) ? 1 : -1;
+
+ let offset = this.depth * -100;
+ if (Language.get('wcf.global.pageDirection') === 'rtl') {
+ // reverse logic for RTL
+ offset *= -1;
+ }
+
+ const child = this.menu.children[0] as HTMLElement;
+ child.style.setProperty('transform', `translateX(${offset}%)`, '');
+ }
+
+ private updateButtonState(): void {
+ let hasNewContent = false;
+ const itemList = this.menu.querySelector('.menuOverlayItemList');
+ this.menu.querySelectorAll('.badgeUpdate').forEach(badge => {
+ const value = badge.textContent!;
+ if (~~value > 0 && badge.closest('.menuOverlayItemList') === itemList) {
+ hasNewContent = true;
+ }
+ });
+
+ this.button.classList[hasNewContent ? 'add' : 'remove']('pageMenuMobileButtonHasContent');
+ }
+}
+
+export = UiPageMenuAbstract;