From 552853f5552489d18de713beb2aedc5126ac700a Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 1 Oct 2016 10:25:37 +0200 Subject: [PATCH] Updated tab menu implementation for mobile devices --- .../files/js/WoltLabSuite/Core/Ui/TabMenu.js | 159 +++++++++++- .../js/WoltLabSuite/Core/Ui/TabMenu/Simple.js | 5 + wcfsetup/install/files/style/ui/tabMenu.scss | 240 ++++++++---------- 3 files changed, 271 insertions(+), 133 deletions(-) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu.js index 8340ef5ce0..59704c5d04 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu.js @@ -6,11 +6,13 @@ * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Ui/TabMenu */ -define(['Dictionary', 'Dom/ChangeListener', 'Dom/Util', 'Ui/CloseOverlay', './TabMenu/Simple'], function(Dictionary, DomChangeListener, DomUtil, UiCloseOverlay, SimpleTabMenu) { +define(['Dictionary', 'EventHandler', 'Dom/ChangeListener', 'Dom/Util', 'Ui/CloseOverlay', 'Ui/Screen', './TabMenu/Simple'], function(Dictionary, EventHandler, DomChangeListener, DomUtil, UiCloseOverlay, UiScreen, SimpleTabMenu) { "use strict"; var _activeList = null; + var _enableTabScroll = false; var _tabMenus = new Dictionary(); + var _scrollListenerUuid = null; /** * @exports WoltLabSuite/Core/Ui/TabMenu @@ -31,6 +33,13 @@ define(['Dictionary', 'Dom/ChangeListener', 'Dom/Util', 'Ui/CloseOverlay', './Ta _activeList = null; } }); + + //noinspection JSUnresolvedVariable + UiScreen.on('screen-sm-down', { + enable: this._scrollEnable.bind(this, false), + disable: this._scrollDisable.bind(this), + setup: this._scrollEnable.bind(this, true) + }); }, /** @@ -75,6 +84,21 @@ define(['Dictionary', 'Dom/ChangeListener', 'Dom/Util', 'Ui/CloseOverlay', './Ta } }); })(list); + + // bind scroll listener + elBySelAll('.tabMenu, .menu', container, (function(menu) { + var callback = this._rebuildMenuOverflow.bind(this, menu); + + var timeout = null; + elBySel('ul', menu).addEventListener('scroll', function () { + if (timeout !== null) { + window.clearTimeout(timeout); + } + + // slight delay to avoid calling this function too often + timeout = window.setTimeout(callback, 10); + }); + }).bind(this)); } } }, @@ -103,6 +127,139 @@ define(['Dictionary', 'Dom/ChangeListener', 'Dom/Util', 'Ui/CloseOverlay', './Ta */ getTabMenu: function(containerId) { return _tabMenus.get(containerId); + }, + + _scrollEnable: function (isSetup) { + _enableTabScroll = true; + + if (!isSetup) { + _tabMenus.forEach((function (tabMenu) { + this.scrollToTab(tabMenu.getActiveTab()); + }).bind(this)); + } + }, + + _scrollDisable: function () { + _enableTabScroll = false; + }, + + scrollToTab: function (tab) { + if (!_enableTabScroll) { + return; + } + + var list = tab.closest('ul'); + var width = list.clientWidth; + var scrollLeft = list.scrollLeft; + var scrollWidth = list.scrollWidth; + if (width === scrollWidth) { + // no overflow, ignore + return; + } + + // check if tab is currently visible + var left = tab.offsetLeft; + var shouldScroll = false; + if (left < scrollLeft) { + shouldScroll = true; + } + + var paddingRight = false; + if (!shouldScroll) { + var visibleWidth = width - (left - scrollLeft); + var virtualWidth = tab.clientWidth; + if (tab.nextElementSibling !== null) { + paddingRight = true; + virtualWidth += 20; + } + + if (visibleWidth < virtualWidth) { + shouldScroll = true; + } + } + + if (shouldScroll) { + // allow some padding to indicate overflow + if (paddingRight) { + left -= 15; + } + else if (left > 0) { + left -= 15; + } + + if (left < 0) { + left = 0; + } + else { + // ensure that our left value is always within the boundaries + left = Math.min(left, scrollWidth - width); + } + + if (scrollLeft === left) { + return; + } + + list.classList.add('enableAnimation'); + + // new value is larger, we're scrolling towards the end + if (scrollLeft < left) { + list.firstElementChild.style.setProperty('margin-left', (scrollLeft - left) + 'px', ''); + } + else { + // new value is smaller, we're scrolling towards the start + list.style.setProperty('padding-left', (scrollLeft - left) + 'px', ''); + } + + setTimeout(function () { + list.classList.remove('enableAnimation'); + + list.firstElementChild.style.removeProperty('margin-left'); + list.style.removeProperty('padding-left'); + + list.scrollLeft = left; + }, 300); + } + }, + + _rebuildMenuOverflow: function (menu) { + if (!_enableTabScroll) { + return; + } + + var width = menu.clientWidth; + var list = elBySel('ul', menu); + var scrollLeft = list.scrollLeft; + var scrollWidth = list.scrollWidth; + + var overflowLeft = (scrollLeft > 0); + var overlayLeft = elBySel('.tabMenuOverlayLeft', menu); + if (overflowLeft) { + if (overlayLeft === null) { + overlayLeft = elCreate('span'); + overlayLeft.className = 'tabMenuOverlayLeft icon icon24 fa-angle-left'; + menu.insertBefore(overlayLeft, menu.firstChild); + } + + overlayLeft.classList.add('active'); + } + else if (overlayLeft !== null) { + overlayLeft.classList.remove('active'); + } + + var overflowRight = (width + scrollLeft < scrollWidth); + var overlayRight = elBySel('.tabMenuOverlayRight', menu); + if (overflowRight) { + if (overlayRight === null) { + overlayRight = elCreate('span'); + overlayRight.className = 'tabMenuOverlayRight icon icon24 fa-angle-right'; + menu.appendChild(overlayRight); + } + + overlayRight.classList.add('active'); + } + else if (overlayRight !== null) { + overlayRight.classList.remove('active'); + } } }; }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu/Simple.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu/Simple.js index 3239fea2b3..3bc3b93a4d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu/Simple.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu/Simple.js @@ -270,6 +270,11 @@ define(['Dictionary', 'EventHandler', 'Dom/Traverse', 'Dom/Util'], function(Dict window.location.href.replace(/#[^#]+$/, '') + '#' + name ); } + + require(['WoltLabSuite/Core/Ui/TabMenu'], function (UiTabMenu) { + //noinspection JSUnresolvedFunction + UiTabMenu.scrollToTab(tab); + }); }, /** diff --git a/wcfsetup/install/files/style/ui/tabMenu.scss b/wcfsetup/install/files/style/ui/tabMenu.scss index 80d6c8b076..658a616e3c 100644 --- a/wcfsetup/install/files/style/ui/tabMenu.scss +++ b/wcfsetup/install/files/style/ui/tabMenu.scss @@ -1,164 +1,95 @@ /* main tabs */ -.tabMenu { - position: relative; - - > ul > li > a { - display: block; - padding: 5px 0; +.tabMenu, +.menu { + > ul { + @include inlineList; - @include wcfFontSection; - } - - @include screen-md-up { - > ul { - border-bottom: 1px solid $wcfContentBorderInner; + > li { + position: relative; - @include inlineList; + &:not(:last-child) { + margin-right: 20px; + } - > li { - position: relative; - - &:not(:last-child) { - margin-right: 20px; - } - - &::before { - border-top: 1px solid $wcfContentLink; - bottom: -1px; - content: ""; - left: 50%; - position: absolute; - width: 0; - } - - &.active::before { - left: 0; - transition: left .12s linear, width .12s linear; - width: 100%; - } - - &.active > a { - cursor: default; - } + &::before { + border-top: 1px solid $wcfContentLink; + bottom: 0; + content: ""; + left: 50%; + position: absolute; + width: 0; + } + + &.active::before { + left: 0; + transition: left .12s linear, width .12s linear; + width: 100%; + } + + &.active > a { + cursor: default; + } + + > a { + display: block; + padding: 5px 0; } } } @include screen-sm-down { + padding-left: 15px; + padding-right: 15px; + position: relative; + + &::before { + display: none; + } + > ul { - border-bottom: 1px solid $wcfContentLink; - display: block; + flex-wrap: nowrap; + overflow: auto; + -webkit-overflow-scrolling: touch; - &:not(.active) > li:not(.active) { - display: none; + > li { + flex-shrink: 0; + white-space: nowrap; } - > li { - padding: 5px 0; + &.enableAnimation { + transition: padding-left .24s linear; - &.active { - pointer-events: none; - - > a::after { - content: $fa-var-caret-down; - font-family: FontAwesome; - margin-left: 7px; - } + > li:first-child { + transition: margin-left .24s linear; } } } - - > span { - display: none; + } + + @include screen-md-up { + > ul { + border-bottom: 1px solid $wcfContentBorderInner; + + > li::before { + bottom: -1px; + } } } } -.tabMenuContent.hidden { - display: none; -} - -.tabMenuContent { - // remove upper border if containerList is the first child - > .containerList:first-child > li:first-child { - border-top-width: 0; +.tabMenu { + > ul > li > a { + @include wcfFontSection; } } /* sub tabs */ .menu { margin-top: 10px; - position: relative; @include screen-md-up { - > ul { - border-bottom: 1px solid $wcfContentBorderInner; - - @include inlineList; - - > li { - position: relative; - - &::before { - border-top: 1px solid $wcfContentLink; - bottom: -1px; - content: ""; - left: 50%; - position: absolute; - width: 0; - } - - &.active::before { - left: 0; - transition: left .12s linear, width .12s linear; - width: 100%; - } - - &:not(:last-child) { - margin-right: 20px; - } - - &.active > a { - cursor: default; - } - - > a { - display: block; - padding: 5px 0; - - @include wcfFontHeadline; - } - } - } - } - - @include screen-sm-down { - > ul { - border-bottom: 1px solid $wcfContentLink; - display: block; - padding-bottom: 5px; - - &:not(.active) > li:not(.active) { - display: none; - } - - > li { - padding: 5px 0; - - &.active { - pointer-events: none; - - > a::after { - content: $fa-var-caret-down; - font-family: FontAwesome; - margin-left: 7px; - } - } - } - } - - > span { - display: none; + > ul > li > a { + @include wcfFontHeadline; } } @@ -166,3 +97,48 @@ margin-top: 20px; } } + +.tabMenuOverlayLeft, +.tabMenuOverlayRight { + align-items: center; + bottom: 0; + display: flex; + height: 100%; + opacity: 0; + position: absolute; + top: 0; + transition: opacity .24s linear, visibility 0s linear .24s; + visibility: hidden; + width: 15px; + z-index: 50; + + &.active { + opacity: 1; + transition-delay: 0s; + visibility: visible; + } + + &::before { + color: $wcfContentDimmedText; + } +} + +.tabMenuOverlayLeft { + left: 0; +} + +.tabMenuOverlayRight { + justify-content: flex-end; + right: 0; +} + +.tabMenuContent.hidden { + display: none; +} + +.tabMenuContent { + // remove upper border if containerList is the first child + > .containerList:first-child > li:first-child { + border-top-width: 0; + } +} -- 2.20.1