Updated tab menu implementation for mobile devices
authorAlexander Ebert <ebert@woltlab.com>
Sat, 1 Oct 2016 08:25:37 +0000 (10:25 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 1 Oct 2016 08:25:44 +0000 (10:25 +0200)
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu/Simple.js
wcfsetup/install/files/style/ui/tabMenu.scss

index 8340ef5ce06f8ec63341b3d3c3231cf6b1dec4ee..59704c5d04f8d3d7abd0f72950dd9798fc703925 100644 (file)
@@ -6,11 +6,13 @@
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @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');
+                       }
                }
        };
 });
index 3239fea2b3b4973ae9ecda1d3540f0e754c7fea8..3bc3b93a4d91e7616500263f713724f9ea366bc2 100644 (file)
@@ -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);
+                       });
                },
                
                /**
index 80d6c8b0765148d55cd38099a161b6d76e32c6ff..658a616e3c457e34b01b9dfb8f9017b7b3fa148a 100644 (file)
 /* 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;
                }
        }
        
                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;
+       }
+}