From 9f39f74cc111d86a480a0340ede7153c74504f3d Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 1 Sep 2016 06:27:32 +0200 Subject: [PATCH] Allow to open the userMenuMobile / mainMenuMobile by swiping on Android --- .../Core/Ui/Page/Menu/Abstract.js | 153 +++++++++++++++++- .../install/files/style/ui/menuMobile.scss | 26 +++ 2 files changed, 178 insertions(+), 1 deletion(-) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Abstract.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Abstract.js index 4cf5f5d906..4aef7491c2 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Abstract.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Abstract.js @@ -6,10 +6,18 @@ * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Ui/Page/Menu/Abstract */ -define(['Environment', 'EventHandler', 'ObjectMap', 'Dom/Traverse', 'Dom/Util', 'Ui/Screen'], function(Environment, EventHandler, ObjectMap, DomTraverse, DomUtil, UiScreen) { +define(['Core', 'Environment', 'EventHandler', 'ObjectMap', 'Dom/Traverse', 'Dom/Util', 'Ui/Screen'], function(Core, Environment, EventHandler, 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 @@ -78,6 +86,10 @@ define(['Environment', 'EventHandler', 'ObjectMap', 'Dom/Traverse', 'Dom/Util', DomUtil.insertAfter(backdrop, this._menu); this._updateButtonState(); + + if (Environment.platform() === 'android') { + this._initializeAndroid(); + } }, /** @@ -150,6 +162,145 @@ define(['Environment', 'EventHandler', 'ObjectMap', 'Dom/Traverse', 'Dom/Util', this.close(true); }, + /** + * Initializes the Android Touch Menu. + */ + _initializeAndroid: function() { + var appearsAt, backdrop, touchStart; + + // 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].pageX < 20); + isRightEdge = isOpen && (Math.abs(this._menu.offsetWidth - touches[0].pageX) < 20); + } + else if (appearsAt === 'right') { + isLeftEdge = isOpen && (Math.abs(document.body.clientWidth - this._menu.offsetWidth - touches[0].pageX) < 20); + isRightEdge = !isOpen && ((document.body.clientWidth - touches[0].pageX) < 20); + } + + // 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 (document.documentElement.classList.contains('pageOverlayActive')) { + 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; + } + + touchStart = touches[0].pageX; + + 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')) return; + + // last known position of the finger + var position; + if (event) { + position = event.changedTouches[0].pageX; + } + else { + position = touchStart; + } + + // clean up touch styles + this._menu.classList.add('androidMenuTouchEnd'); + this._menu.style.removeProperty('transform'); + backdrop.style.removeProperty(appearsAt); + setTimeout((function() { + this._menu.classList.remove('androidMenuTouchEnd'); + }).bind(this), 300); + + // check whether the user moved the finger far enough + if (appearsAt === 'left') { + if (_androidTouching === 'left' && position < touchStart + 100) this.close(); + if (_androidTouching === 'right' && position < touchStart - 100) this.close(); + } + else if (appearsAt === 'right') { + if (_androidTouching === 'left' && position > touchStart + 100) this.close(); + if (_androidTouching === 'right' && position > touchStart - 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; + if (_androidTouching === 'left') movedFromEdge = touches[0].pageX > (touchStart + 5); + if (_androidTouching === 'right') movedFromEdge = touches[0].pageX < (touchStart - 5); + + var isOpen = this._menu.classList.contains('open'); + + if (!isOpen && movedFromEdge) { + // 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].pageX; + 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. * diff --git a/wcfsetup/install/files/style/ui/menuMobile.scss b/wcfsetup/install/files/style/ui/menuMobile.scss index b657edfcb5..4171d797c5 100644 --- a/wcfsetup/install/files/style/ui/menuMobile.scss +++ b/wcfsetup/install/files/style/ui/menuMobile.scss @@ -17,6 +17,32 @@ } } } + + &.androidMenuTouchEnd { + display: block; + position: fixed; + transition: transform .24s cubic-bezier(.25, .46, .45, .94); + + &.pageMainMenuMobile:not(.open) { + transform: translateX(-100vw); + } + &.pageUserMenuMobile:not(.open) { + transform: translateX(100vw); + } + + @include screen-sm-up { + &.pageMainMenuMobile:not(.open) { + transform: translateX(-350px); + } + &.pageUserMenuMobile:not(.open) { + transform: translateX(350px); + } + + + .menuOverlayMobileBackdrop { + transition: left .24s cubic-bezier(.25, .46, .45, .94), right .24s cubic-bezier(.25, .46, .45, .94); + } + } + } > .menuOverlayItemList { // we use `transform: translateX()` for performance reasons -- 2.20.1