Very basic responsive design / fullscreen touch menus
authorAlexander Ebert <ebert@woltlab.com>
Fri, 26 Feb 2016 11:47:46 +0000 (12:47 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Fri, 26 Feb 2016 11:47:46 +0000 (12:47 +0100)
33 files changed:
com.woltlab.wcf/templates/footer.tpl
com.woltlab.wcf/templates/headIncludeJavaScript.tpl
com.woltlab.wcf/templates/pageHeaderLogo.tpl
com.woltlab.wcf/templates/pageMenuMobile.tpl [new file with mode: 0644]
wcfsetup/install/files/js/WCF.ImageViewer.js
wcfsetup/install/files/js/WCF.Location.js
wcfsetup/install/files/js/WCF.User.js
wcfsetup/install/files/js/WCF.js
wcfsetup/install/files/js/WoltLab/WCF/Dom/Traverse.js
wcfsetup/install/files/js/WoltLab/WCF/Dom/Util.js
wcfsetup/install/files/js/WoltLab/WCF/Environment.js
wcfsetup/install/files/js/WoltLab/WCF/Template.js
wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js
wcfsetup/install/files/js/WoltLab/WCF/Ui/Mobile.js
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Abstract.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Main.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/User.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Screen.js [new file with mode: 0644]
wcfsetup/install/files/js/closest.js [new file with mode: 0644]
wcfsetup/install/files/js/require.config.js
wcfsetup/install/files/js/wcf.globalHelper.js
wcfsetup/install/files/style/bootstrap/mixin.scss
wcfsetup/install/files/style/layout/global.scss
wcfsetup/install/files/style/layout/layout.scss
wcfsetup/install/files/style/layout/pageHeader.scss
wcfsetup/install/files/style/layout/pageHeaderSticky.scss
wcfsetup/install/files/style/layout/pageNavigation.scss
wcfsetup/install/files/style/ui/dialog.scss
wcfsetup/install/files/style/ui/dropdown.scss
wcfsetup/install/files/style/ui/dropdownInteractive.scss
wcfsetup/install/files/style/ui/menuMobile.scss [new file with mode: 0644]
wcfsetup/install/files/style/ui/redactor.scss
wcfsetup/install/files/style/ui/tabMenuMessage.scss

index 785fbc7d32c67eac9f2614b046a813041a4631b4..e850feb94153f5224fec4bd622590d7b5e3ec044 100644 (file)
@@ -80,7 +80,9 @@
        
        {include file='pageFooter'}
 </div>
-
+                               
+{include file='pageMenuMobile'}
+                               
 {event name='footer'}
 
 <!-- JAVASCRIPT_RELOCATE_POSITION -->
index 1e158c81d1911aa3a42d3b1639c732bb4056d25d..01ec621c936e4a836a6785f506e44a7c30d48834 100644 (file)
@@ -15,6 +15,7 @@
 {js application='wcf' file='require.config' bundle='WCF.Core' core='true'}
 {js application='wcf' file='require.linearExecution' bundle='WCF.Core' core='true'}
 {js application='wcf' file='wcf.globalHelper' bundle='WCF.Core' core='true'}
+{js application='wcf' file='closest' bundle='WCF.Core' core='true'}
 <script>
 requirejs.config({
        baseUrl: '{@$__wcf->getPath()}js'
index 0bf451bacc62ab133d7989493b5d28881a1733bf..b24717320054155ed463d93bc0037e9025b38229 100644 (file)
@@ -1,10 +1,10 @@
-<div id="logo" class="logo">
+<div id="pageHeaderLogo" class="pageHeaderLogo">
        {if MODULE_WCF_AD && $__disableAds|empty}{@$__wcf->getAdHandler()->getAds('com.woltlab.wcf.logo')}{/if}
        
        <a href="{link}{/link}">
                {* @TODO *}
-               <img src="{@$__wcf->getPath()}images/default-logo.png" alt="" class="large">
-               <img src="{@$__wcf->getPath()}images/default-logo-small.png" alt="" class="small">
+               <img src="{@$__wcf->getPath()}images/default-logo.png" alt="" class="pageHeaderLogoLarge">
+               <img src="{@$__wcf->getPath()}images/default-logo-small.png" alt="" class="pageHeaderLogoSmall">
                {*if $__wcf->getStyleHandler()->getStyle()->getPageLogo()}
                        <img src="{$__wcf->getStyleHandler()->getStyle()->getPageLogo()}" alt="">
                {/if*}
diff --git a/com.woltlab.wcf/templates/pageMenuMobile.tpl b/com.woltlab.wcf/templates/pageMenuMobile.tpl
new file mode 100644 (file)
index 0000000..d798e52
--- /dev/null
@@ -0,0 +1,135 @@
+{* main menu / page options / breadcrumbs *}
+<div id="pageMainMenuMobile" class="pageMainMenuMobile menuOverlayMobile" data-page-logo="{$__wcf->getPath()}images/default-logo.png">
+       <ol class="menuOverlayItemList" data-title="TODO: menu">
+               <li class="menuOverlayTitle">TODO: menu</li>
+               <li class="menuOverlayItem">
+                       <a href="#" class="menuOverlayItemLink box24">
+                               <span class="icon icon24 fa-sitemap"></span>
+                               <span class="menuOverlayItemTitle">TODO: navigation</span>
+                       </a>
+                       <ol class="menuOverlayItemList">
+                               {foreach from=$__wcf->getBoxHandler()->getBoxes('mainMenu')[0]->getMenu()->getMenuItemNodeList() item=menuItemNode}
+                               <li class="menuOverlayItem">
+                                       {assign var=__outstandingItems value=$menuItemNode->getMenuItem()->getOutstandingItems()}
+                                       <a href="{$menuItemNode->getMenuItem()->getURL()}" class="menuOverlayItemLink{if $__outstandingItems} menuOverlayItemBadge{/if}">
+                                               <span class="menuOverlayItemTitle">{lang}{$menuItemNode->getMenuItem()->title}{/lang}</span>
+                                               {if $__outstandingItems}
+                                                       <span class="badge badgeInverse">{#$__outstandingItems}</span>
+                                               {/if}
+                                       </a>
+                                       
+                                       {if $menuItemNode->hasChildren()}<ol class="menuOverlayItemList">{else}</li>{/if}
+                                               
+                                               {if !$menuItemNode->hasChildren() && $menuItemNode->isLastSibling()}
+                                                       {@"</ol></li>"|str_repeat:$menuItemNode->getOpenParentNodes()}
+                                               {/if}
+                                               {/foreach}
+                                       </ol>
+                               </li>
+                               {hascontent}
+                                       <li class="menuOverlayItem">
+                                               <a href="#" class="menuOverlayItemLink box24">
+                                                       <span class="icon icon24 fa-gears"></span>
+                                                       <span class="menuOverlayItemTitle">TODO: page options</span>
+                                               </a>
+                                               <ol class="menuOverlayItemList">
+                                                       {content}
+                                                               {if !$__pageOptions|empty}
+                                                                       {@$__pageOptions}
+                                                               {/if}
+                                                               
+                                                               {event name='pageOptions'}
+                                                       {/content}
+                                               </ol>
+                                       </li>
+                               {/hascontent}
+                               {hascontent}
+                                       <li class="menuOverlayTitle">TODO: current location</li>
+                                       <li class="menuOverlayItem">
+                                               <a href="#" class="menuOverlayItemLink box24">
+                                                       <span class="icon icon24 fa-cogs"></span>
+                                                       <span class="menuOverlayItemTitle">TODO: current location</span>
+                                               </a>
+                                               <ol class="menuOverlayItemList">
+                                                       {content}
+                                                       {assign var=__breadcrumbsDepth value=0}
+                                                       {foreach from=$__wcf->getBreadcrumbs() item=$breadcrumb}
+                                                               <li class="menuOverlayItem">
+                                                                       <a href="{$breadcrumb->getURL()}" class="menuOverlayItemLink">
+                                                                               <span class="menuOverlayItemTitle"{if $__breadcrumbsDepth} style="padding-left: {$__breadcrumbsDepth * 10}px" {/if}>
+                                                                                       <span class="icon icon24 fa-{if $__breadcrumbsDepth}caret-right{else}home{/if}"></span>
+                                                                                       {$breadcrumb->getLabel()}
+                                                                               </span>
+                                                                       </a>
+                                                               </li>
+                                                               {assign var=__breadcrumbsDepth value=$__breadcrumbsDepth + 1}
+                                                       {/foreach}
+                                                       {/content}
+                                               </ol>
+                                       </li>
+                               {/hascontent}
+                       </ol>
+               </li>
+       </ol>
+</div>
+
+{* user menu *}
+{* TODO: guests should see the login overlay when clicking the button *}
+<div id="pageUserMenuMobile" class="pageUserMenuMobile menuOverlayMobile" data-page-logo="{$__wcf->getPath()}images/default-logo.png">
+       <ol class="menuOverlayItemList" data-title="TODO: user menu">
+               <li class="menuOverlayTitle">{lang}wcf.user.controlPanel{/lang}</li>
+               <li class="menuOverlayItem">
+                       <a href="{link controller='User' object=$__wcf->user}{/link}" class="menuOverlayItemLink box24">
+                               {@$__wcf->getUserProfileHandler()->getAvatar()->getImageTag(24)}
+                               <span class="menuOverlayItemTitle">{$__wcf->user->username}</span>
+                       </a>
+               </li>
+               <li class="menuOverlayItem">
+                       <a href="{link controller='Settings'}{/link}" class="menuOverlayItemLink box24">
+                               <span class="icon icon24 fa-cog"></span>
+                               <span class="menuOverlayItemTitle">Einstellungen</span>
+                       </a>
+                       <ol class="menuOverlayItemList">
+                               {foreach from=$__wcf->getUserMenu()->getMenuItems('') item=menuCategory}
+                                       <li class="menuOverlayTitle">{lang}{$menuCategory->menuItem}{/lang}</li>
+                                       {foreach from=$__wcf->getUserMenu()->getMenuItems($menuCategory->menuItem) item=menuItem}
+                                               <li class="menuOverlayItem">
+                                                       <a href="{$menuItem->getProcessor()->getLink()}" class="menuOverlayItemLink">{@$menuItem}</a>
+                                               </li>
+                                       {/foreach}
+                               {/foreach}
+                       </ol>
+               </li>
+               {if $__wcf->session->getPermission('admin.general.canUseAcp')}
+                       <li class="menuOverlayItem">
+                               <a href="{link isACP=true}{/link}" class="menuOverlayItemLink box24">
+                                       <span class="icon icon24 fa-wrench"></span>
+                                       <span class="menuOverlayItemTitle">{lang}wcf.global.acp.short{/lang}</span>
+                               </a>
+                       </li>
+               {/if}
+               <li class="menuOverlayItemSpacer"></li>
+               <li class="menuOverlayItem" data-more="com.woltlab.wcf.notifications">
+                       <a href="{link controller='NotificationList'}{/link}" class="menuOverlayItemLink box24">
+                               <span class="icon icon24 fa-bell-o"></span>
+                               <span class="menuOverlayItemTitle">{lang}wcf.user.notification.notifications{/lang}</span>
+                       </a>
+               </li>
+               <li class="menuOverlayItem">
+                       <a href="#" class="menuOverlayItemLink box24">
+                               <span class="icon icon24 fa-exclamation-triangle"></span>
+                               <span class="menuOverlayItemTitle">{lang}wcf.moderation.moderation{/lang}</span>
+                       </a>
+               </li>
+               
+               {event name='userMenuItems'}
+               
+               <li class="menuOverlayItemSpacer"></li>
+               <li class="menuOverlayItem">
+                       <a href="{link controller='Logout'}t={@SECURITY_TOKEN}{/link}" class="menuOverlayItemLink box24">
+                               <span class="icon icon24 fa-sign-out"></span>
+                               <span class="menuOverlayItemTitle">{lang}wcf.user.logout{/lang}</span>
+                       </a>
+               </li>
+       </ol>
+</div>
index 60b90ac5526e188dd36808d4124f54e6378bd25d..84b40172fac9d6a956813b5408b9964bcbcb0053 100644 (file)
@@ -874,7 +874,7 @@ $.widget('ui.wcfImageViewer', {
                
                WCF.DOMNodeInsertedHandler.execute();
                
-               enquire.register('screen and (max-width: 800px)', {
+               enquire.register('(max-width: 767px)', {
                        match: $.proxy(this._enableMobileView, this),
                        unmatch: $.proxy(this._disableMobileView, this)
                });
index fd5961e7c42887be35a97b69eeedc86f9e85aaff..070ca5ee69a96b677e41e4ac16fd79a4eaf79a8c 100644 (file)
@@ -123,7 +123,7 @@ WCF.Location.GoogleMaps.Map = Class.extend({
                // fix maps in mobile sidebars by refreshing the map when displaying
                // the map
                if (this._mapContainer.parents('.sidebar').length) {
-                       enquire.register('screen and (max-width: 800px)', {
+                       enquire.register('(max-width: 767px)', {
                                setup: $.proxy(this._addSidebarMapListener, this),
                                deferSetup: true
                        });
index 5dbf497195e769d0d583060b621d7dc9edf0cce6..5658d49c1155e6a8c686f3322e619e8fdc80dd26 100644 (file)
@@ -201,11 +201,13 @@ WCF.User.Panel.Abstract = Class.extend({
        /**
         * Toggles the interactive dropdown.
         * 
-        * @param       object          event
-        * @return      boolean
+        * @param       {Event=}                event
+        * @return      {boolean}
         */
        toggle: function(event) {
-               event.preventDefault();
+               if (event instanceof Event) {
+                       event.preventDefault();
+               }
                
                if (this._dropdown === null) {
                        this._dropdown = this._initDropdown();
@@ -445,6 +447,18 @@ WCF.User.Panel.Notification = WCF.User.Panel.Abstract.extend({
                }
                
                WCF.System.PushNotification.addCallback('userNotificationCount', $.proxy(this.updateUserNotificationCount, this));
+               
+               require(['EventHandler'], (function(EventHandler) {
+                       EventHandler.add('com.woltlab.wcf.UserMenuMobile', 'more', (function(data) {
+                               console.debug("called");
+                               console.debug(data);
+                               if (data.identifier === 'com.woltlab.wcf.notifications') {
+                                       this.toggle();
+                                       
+                                       //data.handler.close(true);
+                               }
+                       }).bind(this));
+               }).bind(this));
        },
        
        /**
index 5672aec6a1d5cac040311d82f21f299444ba7dbb..5a8d5df987bf78e0334fa92695d8b575c96ae362 100755 (executable)
@@ -1225,13 +1225,20 @@ WCF.Dropdown.Interactive.Instance = Class.extend({
         * Renders the dropdown.
         */
        render: function() {
-               var $pageDirection = WCF.Language.get('wcf.global.pageDirection');
-               
-               if ($('html').css('caption-side') === 'bottom') {
-                       this._renderMobile($pageDirection);
+               if (window.matchMedia('(max-width: 767px)').matches) {
+                       this._container.css({
+                               bottom: '',
+                               left: '',
+                               right: '',
+                               top: elById('pageHeader').clientHeight + 'px'
+                       });
                }
                else {
-                       this._renderDesktop($pageDirection);
+                       require(['Ui/Alignment'], (function(UiAlignment) {
+                               UiAlignment.set(this._container[0], this._triggerElement[0], {
+                                       pointer: true
+                               });
+                       }).bind(this));
                }
        },
        
@@ -1248,39 +1255,6 @@ WCF.Dropdown.Interactive.Instance = Class.extend({
                                suppressScrollX: true
                        });
                }
-       },
-       
-       /**
-        * Renders the dropdown on mobile devices.
-        * 
-        * @param       string          pageDirection
-        */
-       _renderMobile: function(pageDirection) {
-               var $elementDimensions = this._triggerElement.getDimensions('outer');
-               var $elementHalfWidth = Math.floor($elementDimensions.width / 2);
-               var $elementOffsets = this._triggerElement.getOffsets('offset');
-               var $pointerHalfWidth = Math.floor(this._pointer.outerWidth() / 2);
-               
-               this._container.css({
-                       top: $elementOffsets.top + $elementDimensions.height + 'px'
-               });
-               
-               this._pointer.css({
-                       left: ($elementOffsets.left + $elementHalfWidth) - $pointerHalfWidth + 'px'
-               });
-       },
-       
-       /**
-        * Renders the dropdown on desktops.
-        * 
-        * @param       string          pageDirection
-        */
-       _renderDesktop: function(pageDirection) {
-               require(['Ui/Alignment'], (function(UiAlignment) {
-                       UiAlignment.set(this._container[0], this._triggerElement[0], {
-                               pointer: true
-                       });
-               }).bind(this));
        }
 });
 
index 1967cd7de22d8a9330a92f0b59863b92f4bae209..428ea3ea3df4a017476ad1324a0bf15151e261da 100644 (file)
@@ -2,11 +2,11 @@
  * Provides helper functions to traverse the DOM.
  * 
  * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
+ * @copyright  2001-2016 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module     WoltLab/WCF/Dom/Traverse
  */
-define(['Dom/Util'], function(DomUtil) {
+define([], function() {
        "use strict";
        
        /** @const */ var NONE = 0;
@@ -16,7 +16,7 @@ define(['Dom/Util'], function(DomUtil) {
        
        var _probe = [
                function(el, none) { return true; },
-               function(el, selector) { return DomUtil.matches(el, selector); },
+               function(el, selector) { return el.matches(selector); },
                function(el, className) { return el.classList.contains(className); },
                function(el, tagName) { return el.nodeName === tagName; }
        ];
@@ -76,7 +76,7 @@ define(['Dom/Util'], function(DomUtil) {
        /**
         * @exports     WoltLab/WCF/Dom/Traverse
         */
-       var DomTraverse = {
+       return {
                /**
                 * Examines child elements and returns the first child matching the given selector.
                 * 
@@ -215,11 +215,11 @@ define(['Dom/Util'], function(DomUtil) {
                 * Returns the next element sibling with given CSS class.
                 * 
                 * @param       {Element}       el              element
-                * @param       {string}        className       CSS class name
+                * @param       {string}        tagName         element tag name
                 * @return      {(Element|null)}        null if there is no next sibling element or it does not have the class set
                 */
                nextByTag: function(el, tagName) {
-                       return _sibling(el, 'nextElementSibling', CLASS_NAME, className);
+                       return _sibling(el, 'nextElementSibling', TAG_NAME, tagName);
                },
                
                /**
@@ -258,13 +258,11 @@ define(['Dom/Util'], function(DomUtil) {
                 * Returns the previous element sibling with given CSS class.
                 * 
                 * @param       {Element}       el              element
-                * @param       {string}        className       CSS class name
+                * @param       {string}        tagName         element tag name
                 * @return      {(Element|null)}        null if there is no previous sibling element or it does not have the class set
                 */
                prevByTag: function(el, tagName) {
-                       return _sibling(el, 'previousElementSibling', CLASS_NAME, className);
+                       return _sibling(el, 'previousElementSibling', TAG_NAME, tagName);
                }
        };
-       
-       return DomTraverse;
 });
index 5de5990b68a62dbb97ca2101c8c99a2650d04af6..5186e870570b873f0149df1ccfd01045150a6421 100644 (file)
@@ -9,15 +9,6 @@
 define(['StringUtil'], function(StringUtil) {
        "use strict";
        
-       var _matchesSelectorFunction = '';
-       var _possibleFunctions = ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector'];
-       for (var i = 0; i < 4; i++) {
-               if (Element.prototype.hasOwnProperty(_possibleFunctions[i])) {
-                       _matchesSelectorFunction = _possibleFunctions[i];
-                       break;
-               }
-       }
-       
        function _isBoundaryNode(element, ancestor, position) {
                if (!ancestor.contains(element)) {
                        throw new Error("Ancestor element does not contain target element.");
@@ -93,8 +84,8 @@ define(['StringUtil'], function(StringUtil) {
                 * @return      {string}        element id
                 */
                identify: function(el) {
-                       if (!el || !(el instanceof Element)) {
-                               return null;
+                       if (!(el instanceof Element)) {
+                               throw new TypeError("Expected a valid DOM element as argument.");
                        }
                        
                        var id = elAttr(el, 'id');
@@ -106,17 +97,6 @@ define(['StringUtil'], function(StringUtil) {
                        return id;
                },
                
-               /**
-                * Returns true if element matches given CSS selector.
-                * 
-                * @param       {Element}       el              element
-                * @param       {string}        selector        CSS selector
-                * @return      {boolean}       true if element matches selector
-                */
-               matches: function(el, selector) {
-                       return el[_matchesSelectorFunction](selector);
-               },
-               
                /**
                 * Returns the outer height of an element including margins.
                 * 
index a0fa0e213dfe6ae60b46eb1c28166f563230caa4..97f2872847828184825fe2c609b83cb9dd8de39d 100644 (file)
@@ -2,7 +2,7 @@
  * Provides basic details on the JavaScript environment.
  * 
  * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
+ * @copyright  2001-2016 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module     WoltLab/WCF/Environment
  */
@@ -17,7 +17,7 @@ define([], function() {
        /**
         * @exports     WoltLab/WCF/Enviroment
         */
-       var Environment = {
+       return {
                /**
                 * Determines environment variables.
                 */
@@ -121,6 +121,4 @@ define([], function() {
                        return _touch;
                }
        };
-       
-       return Environment;
 });
index a855d161a6ede59b3e5d4963d08478aca86f00c3..cbe26c5447dd45042d03b1b42792863676c25924 100644 (file)
@@ -27,8 +27,9 @@ define(['./Template.grammar', './StringUtil', 'Language'], function(parser, Stri
         * @constructor
         */
        function Template(template) {
-               // Fetch Language, as it cannot be provided because of a circular dependency
+               // Fetch Language/StringUtil, as it cannot be provided because of a circular dependency
                if (Language === undefined) Language = require('Language');
+               if (StringUtil === undefined) StringUtil = require('StringUtil');
                
                try {
                        template = parser.parse(template);
index 10d4140fcfe9089f4347dff5125ee94255bd8b6c..012413d6f2e5adcbf256261dd641c4a3944ec9d1 100644 (file)
@@ -58,7 +58,7 @@ define(
                                return true;
                        }).bind(this);
                        
-                       enquire.register('screen and (max-width: 800px)', {
+                       enquire.register('(max-width: 767px)', {
                                match: function() { _dialogFullHeight = true; },
                                unmatch: function() { _dialogFullHeight = false; },
                                setup: function() { _dialogFullHeight = true; },
index 9eee9ca29f030e54726fb87311d7e23ca338cb4e..ff41be0b2861e5e83bb854fa70afaab714841c4c 100644 (file)
@@ -2,25 +2,27 @@
  * Modifies the interface to provide a better usability for mobile devices.
  * 
  * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
+ * @copyright  2001-2016 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module     WoltLab/WCF/Ui/Mobile
  */
 define(
-       [       'enquire', 'Environment', 'Language', 'Dom/ChangeListener', 'Dom/Traverse', 'Ui/CloseOverlay'],
-       function(enquire,   Environment,   Language,   DomChangeListener,    DomTraverse,    UiCloseOverlay)
+       [       'Environment', 'Language', 'Dom/ChangeListener', 'Ui/CloseOverlay', 'Ui/Screen', './Page/Menu/Main', './Page/Menu/User'],
+       function(Environment,   Language,   DomChangeListener,    UiCloseOverlay,    UiScreen,    UiPageMenuMain,     UiPageMenuUser)
 {
        "use strict";
        
        var _buttonGroupNavigations = null;
        var _enabled = false;
        var _main = null;
+       var _pageMenuMain = null;
+       var _pageMenuUser = null;
        var _sidebar = null;
        
        /**
         * @exports     WoltLab/WCF/Ui/Mobile
         */
-       var UiMobile = {
+       return {
                /**
                 * Initializes the mobile UI using enquire.js.
                 */
@@ -37,16 +39,11 @@ define(
                                document.documentElement.classList.add('mobile');
                        }
                        
-                       enquire.register('screen and (max-width: 800px)', {
-                               match: this.enable.bind(this),
-                               unmatch: this.disable.bind(this),
-                               setup: this._init.bind(this),
-                               deferSetup: true
+                       UiScreen.on({
+                               small: this.enable.bind(this),
+                               large: this.disable.bind(this),
+                               setup: this._init.bind(this)
                        });
-                       
-                       if (Environment.browser() === 'microsoft' && _sidebar !== null && _sidebar.clientWidth > 305) {
-                               this._fixSidebarIE();
-                       }
                },
                
                /**
@@ -55,7 +52,8 @@ define(
                enable: function() {
                        _enabled = true;
                        
-                       if (Environment.browser() === 'microsoft') this._fixSidebarIE();
+                       _pageMenuMain.enable();
+                       _pageMenuUser.enable();
                },
                
                /**
@@ -64,21 +62,15 @@ define(
                disable: function() {
                        _enabled = false;
                        
-                       if (Environment.browser() === 'microsoft') this._fixSidebarIE();
-               },
-               
-               _fixSidebarIE: function() {
-                       if (_sidebar === null) return;
-                       
-                       // sidebar is rarely broken on IE9/IE10
-                       _sidebar.style.setProperty('display', 'none');
-                       _sidebar.style.removeProperty('display');
+                       _pageMenuMain.disable();
+                       _pageMenuUser.disable();
                },
                
                _init: function() {
-                       this._initSidebarToggleButtons();
-                       this._initSearchBar();
+                       //this._initSidebarToggleButtons();
+                       //this._initSearchBar();
                        this._initButtonGroupNavigation();
+                       this._initMobileMenu();
                        
                        UiCloseOverlay.add('WoltLab/WCF/Ui/Mobile', this._closeAllMenus.bind(this));
                        DomChangeListener.add('WoltLab/WCF/Ui/Mobile', this._initButtonGroupNavigation.bind(this));
@@ -154,22 +146,29 @@ define(
                                span.className = 'icon icon24 fa-list';
                                button.appendChild(span);
                                
-                               button.addEventListener('click', function(ev) {
-                                       var next = DomTraverse.next(button);
-                                       if (next !== null) {
-                                               next.classList.toggle('open');
+                               (function(button) {
+                                       button.addEventListener('click', function(ev) {
+                                               var next = button.nextElementSibling;
+                                               if (next !== null) {
+                                                       next.classList.toggle('open');
+                                                       
+                                                       ev.stopPropagation();
+                                                       return false;
+                                               }
                                                
-                                               ev.stopPropagation();
-                                               return false;
-                                       }
-                                       
-                                       return true;
-                               });
+                                               return true;
+                                       });
+                               })(button);
                                
                                navigation.insertBefore(button, navigation.firstChild);
                        }
                },
                
+               _initMobileMenu: function() {
+                       _pageMenuMain = new UiPageMenuMain();
+                       _pageMenuUser = new UiPageMenuUser();
+               },
+               
                _closeAllMenus: function() {
                        var openMenus = elBySelAll('.jsMobileButtonGroupNavigation > ul.open');
                        for (var i = 0, length = openMenus.length; i < length; i++) {
@@ -177,6 +176,4 @@ define(
                        }
                }
        };
-       
-       return UiMobile;
 });
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Abstract.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Abstract.js
new file mode 100644 (file)
index 0000000..284cd5e
--- /dev/null
@@ -0,0 +1,286 @@
+/**
+ * Provides a touch-friendly fullscreen menu.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Ui/Page/Menu/Abstract
+ */
+define(['Environment', 'EventHandler', 'ObjectMap', 'Dom/Traverse', 'Ui/Screen'], function(Environment, EventHandler, ObjectMap, DomTraverse, UiScreen) {
+       "use strict";
+       
+       /**
+        * @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) {
+                       this._enabled = true;
+                       this._eventIdentifier = eventIdentifier;
+                       this._items = new ObjectMap();
+                       this._menu = elById(elementId);
+                       
+                       var callbackOpen = this.open.bind(this);
+                       var button = elBySel(buttonSelector);
+                       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));
+                       
+                       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));
+               },
+               
+               /**
+                * Opens the menu.
+                * 
+                * @param       {Event}         event   event object
+                */
+               open: function(event) {
+                       if (!this._enabled) {
+                               return;
+                       }
+                       
+                       if (event instanceof Event) {
+                               event.preventDefault();
+                       }
+                       
+                       this._menu.classList.add('enableAnimation');
+                       this._menu.classList.add('open');
+                       
+                       UiScreen.scrollDisable();
+               },
+               
+               /**
+                * Closes the menu.
+                * 
+                * @param       {(Event|boolean)}       event   event object or boolean true to force close the menu
+                */
+               close: function(event) {
+                       if (event instanceof Event) {
+                               event.preventDefault();
+                       }
+                       else if (event === true) {
+                               this._menu.classList.remove('enableAnimation');
+                       }
+                       
+                       if (this._menu.classList.contains('open')) {
+                               this._menu.classList.remove('open');
+                               
+                               UiScreen.scrollEnable();
+                       }
+                       
+               },
+               
+               /**
+                * Enables the touch menu.
+                */
+               enable: function() {
+                       this._enabled = true;
+               },
+               
+               /**
+                * Disables the touch menu.
+                */
+               disable: function() {
+                       this._enabled = false;
+                       
+                       this.close(true);
+               },
+               
+               /**
+                * 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) {
+                       var itemList = item.nextElementSibling;
+                       if (itemList === null) {
+                               return;
+                       }
+                       
+                       var isLink = (elAttr(item, 'href') !== '#');
+                       var parent = item.parentNode;
+                       var parentItemList = parent.parentNode;
+                       var itemTitle = DomTraverse.childByClass(item, 'menuOverlayItemTitle').textContent;
+                       
+                       this._items.set(item, {
+                               itemList: itemList,
+                               parentItemList: parentItemList
+                       });
+                       
+                       elData(itemList, 'title', itemTitle);
+                       
+                       var callbackLink = this._showItemList.bind(this, item), wrapper;
+                       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';
+                               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();
+                       }
+                       
+                       var data = this._items.get(item);
+                       data.itemList.classList.remove('active');
+                       data.parentItemList.classList.remove('hidden');
+               },
+               
+               /**
+                * 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;
+                               }
+                       }
+                       
+                       data.itemList.classList.add('active');
+                       data.parentItemList.classList.add('hidden');
+               }
+       };
+       
+       return UiPageMenuAbstract;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Main.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Main.js
new file mode 100644 (file)
index 0000000..9cedcb3
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Provides the touch-friendly fullscreen main menu.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Ui/Page/Menu/Main
+ */
+define(['Core', './Abstract'], function(Core, UiPageMenuAbstract) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function UiPageMenuMain() { this.init(); }
+       Core.inherit(UiPageMenuMain, UiPageMenuAbstract, {
+               /**
+                * Initializes the touch-friendly fullscreen main menu.
+                */
+               init: function() {
+                       UiPageMenuMain._super.prototype.init.call(
+                               this,
+                               'com.woltlab.wcf.MainMenuMobile',
+                               'pageMainMenuMobile',
+                               '#pageHeader .mainMenu'
+                       );
+               }
+       });
+       
+       return UiPageMenuMain;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/User.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/User.js
new file mode 100644 (file)
index 0000000..dfe75fb
--- /dev/null
@@ -0,0 +1,58 @@
+/**
+ * Provides the touch-friendly fullscreen user menu.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Ui/Page/Menu/User
+ */
+define(['Core', 'EventHandler', './Abstract'], function(Core, EventHandler, UiPageMenuAbstract) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function UiPageMenuUser() { this.init(); }
+       Core.inherit(UiPageMenuUser, UiPageMenuAbstract, {
+               /**
+                * Initializes the touch-friendly fullscreen user menu.
+                */
+               init: function() {
+                       UiPageMenuUser._super.prototype.init.call(
+                               this,
+                               'com.woltlab.wcf.UserMenuMobile',
+                               'pageUserMenuMobile',
+                               '#pageHeader .userPanel'
+                       );
+               },
+               
+               /**
+                * Overrides the `_initItem()` method to check for special items that do not
+                * act as a link but instead trigger an event for external processing.
+                * 
+                * @param       {Element}       item    menu item
+                * @protected
+                */
+               _initItem: function(item) {
+                       // check if it should contain a 'more' link w/ an external callback
+                       var more = elData(item.parentNode, 'more');
+                       if (more) {
+                               item.addEventListener(WCF_CLICK_EVENT, (function(event) {
+                                       event.preventDefault();
+                                       event.stopPropagation();
+                                       
+                                       EventHandler.fire(this._eventIdentifier, 'more', {
+                                               handler: this,
+                                               identifier: more
+                                       });
+                               }).bind(this));
+                               
+                               return;
+                       }
+                       
+                       UiPageMenuUser._super.prototype._initItem.call(this, item);
+               }
+       });
+       
+       return UiPageMenuUser;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Screen.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Screen.js
new file mode 100644 (file)
index 0000000..bb16608
--- /dev/null
@@ -0,0 +1,183 @@
+/**
+ * Provides consistent support for media queries and body scrolling.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Ui/Screen
+ */
+define(['Core', 'Dictionary'], function(Core, Dictionary) {
+       "use strict";
+       
+       var _bodyOverflow = '';
+       var _mql = new Dictionary();
+       var _scrollDisableCounter = 0;
+       
+       /**
+        * @exports     WoltLab/WCF/Ui/Screen
+        */
+       return {
+               /**
+                * Registers event listeners for media query match/unmatch.
+                * 
+                * The `callbacks` object may contain the following keys:
+                *  - `small` or `match`, triggered when media query matches
+                *  - `large` or `unmatch`, triggered when media query no longer matches
+                *  - `setup`, invoked when media query first matches
+                * 
+                * The `small` and `large` keys only exist to increase readability when omitting
+                * the `query` argument and thus default to match the default value of `query`.
+                * 
+                * `query` will default to `(max-width: 767px)`, it allows any value that can
+                * be evaluated with `window.matchMedia`.
+                * 
+                * Returns a UUID that is used to internal identify the callbacks, can be used
+                * to remove binding by calling the `remove` method.
+                * 
+                * @param       {object}        callbacks
+                * @param       {string=}       query
+                * @return      {string}        UUID for listener removal
+                */
+               on: function(callbacks, query) {
+                       var uuid = Core.getUuid(), queryObject = this._getQueryObject(query);
+                       
+                       if (typeof callbacks.small === 'function' || typeof callbacks.match === 'function') {
+                               queryObject.callbacksMatch.set(uuid, callbacks.small || callbacks.match);
+                       }
+                       
+                       if (typeof callbacks.large === 'function' || typeof callbacks.unmatch === 'function') {
+                               queryObject.callbacksUnmatch.set(uuid, callbacks.large || callbacks.unmatch);
+                       }
+                       
+                       if (typeof callbacks.setup === 'function') {
+                               if (queryObject.mql.matches) {
+                                       callbacks.setup();
+                               }
+                               else {
+                                       queryObject.callbacksSetup.set(uuid, callbacks.setup);
+                               }
+                       }
+                       
+                       return uuid;
+               },
+               
+               /**
+                * Removes all listeners identified by their common UUID.
+                * 
+                * @param       {string}        uuid    UUID received when calling `on()`
+                * @param       {string=}       query   must match the `query` argument used when calling `on()`
+                */
+               remove: function(uuid, query) {
+                       var queryObject = this._getQueryObject(query);
+                       
+                       queryObject.callbacksMatch.delete(uuid);
+                       queryObject.callbacksUnmatch.delete(uuid);
+                       queryObject.callbacksSetup.delete(uuid);
+               },
+               
+               /**
+                * Returns a boolean value if a media query expression currently matches.
+                * 
+                * @param       {string=}       query   CSS media query
+                * @returns     {boolean}       true if query matches
+                */
+               is: function(query) {
+                       var queryObject = this._getQueryObject(query);
+                       
+                       if (query === 'large') {
+                               // the query matches for max-width, we need to inverse the logic here
+                               return !queryObject.mql.matches;
+                       }
+                       
+                       return queryObject.mql.matches;
+               },
+               
+               /**
+                * Disables scrolling of body element.
+                */
+               scrollDisable: function() {
+                       if (_scrollDisableCounter === 0) {
+                               _bodyOverflow = document.body.style.getPropertyValue('overflow');
+                               
+                               document.body.style.setProperty('overflow', 'hidden', '');
+                       }
+                       
+                       _scrollDisableCounter++;
+               },
+               
+               /**
+                * Re-enables scrolling of body element.
+                */
+               scrollEnable: function() {
+                       if (_scrollDisableCounter) {
+                               _scrollDisableCounter--;
+                               
+                               if (_scrollDisableCounter === 0) {
+                                       if (_bodyOverflow) {
+                                               document.body.style.setProperty('overflow', _bodyOverflow, '');
+                                       }
+                                       else {
+                                               document.body.style.removeProperty('overflow');
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * 
+                * @param       {string=}       query   CSS media query
+                * @return      {Object}        object containing callbacks and MediaQueryList
+                * @protected
+                */
+               _getQueryObject: function(query) {
+                       if (typeof query !== 'string') query = '';
+                       if (query === '' || query === 'small' || query === 'large') {
+                               query = '(max-width: 767px)';
+                       }
+                       
+                       var queryObject = _mql.get(query);
+                       if (!queryObject) {
+                               queryObject = {
+                                       callbacksMatch: new Dictionary(),
+                                       callbacksUnmatch: new Dictionary(),
+                                       callbacksSetup: new Dictionary(),
+                                       mql: window.matchMedia(query)
+                               };
+                               queryObject.mql.addListener(this._mqlChange.bind(this));
+                               
+                               _mql.set(query, queryObject);
+                       }
+                       
+                       return queryObject;
+               },
+               
+               /**
+                * Triggered whenever a registered media query now matches or no longer matches.
+                * 
+                * @param       {Event} event   event object
+                * @protected
+                */
+               _mqlChange: function(event) {
+                       var queryObject = this._getQueryObject(event.media);
+                       if (event.matches) {
+                               if (queryObject.callbacksSetup.size) {
+                                       queryObject.callbacksSetup.forEach(function(callback) {
+                                               callback();
+                                       });
+                                       
+                                       // discard all setup callbacks after execution
+                                       queryObject.callbacksSetup = new Dictionary();
+                               }
+                               
+                               queryObject.callbacksMatch.forEach(function(callback) {
+                                       callback();
+                               });
+                       }
+                       else {
+                               queryObject.callbacksUnmatch.forEach(function(callback) {
+                                       callback();
+                               });
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/closest.js b/wcfsetup/install/files/js/closest.js
new file mode 100644 (file)
index 0000000..3454c86
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Polyfill for `Element.prototype.matches()` and `Element.prototype.closest()`
+ * Copyright (c) 2015 Jonathan Neal - https://github.com/jonathantneal/closest
+ * License: CC0 1.0 Universal (https://creativecommons.org/publicdomain/zero/1.0/)
+ */
+(function(ELEMENT) {
+       ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector;
+       
+       ELEMENT.closest = ELEMENT.closest || function closest(selector) {
+                       var element = this;
+                       
+                       while (element) {
+                               if (element.matches(selector)) {
+                                       break;
+                               }
+                               
+                               element = element.parentElement;
+                       }
+                       
+                       return element;
+               };
+}(Element.prototype));
index d57e30a0e2fe150bc6137e571e8155a8ff547bf9..0d2d54dd59d2f2c379690293362207a02bcad170 100644 (file)
@@ -34,6 +34,7 @@ requirejs.config({
                        'Ui/Dialog': 'WoltLab/WCF/Ui/Dialog',
                        'Ui/Notification': 'WoltLab/WCF/Ui/Notification',
                        'Ui/ReusableDropdown': 'WoltLab/WCF/Ui/Dropdown/Reusable',
+                       'Ui/Screen': 'WoltLab/WCF/Ui/Screen',
                        'Ui/SimpleDropdown': 'WoltLab/WCF/Ui/Dropdown/Simple',
                        'Ui/TabMenu': 'WoltLab/WCF/Ui/TabMenu',
                        'Upload': 'WoltLab/WCF/Upload'
index b0ac6a8b7ad887d7c6d0dbfc7dcf488c65c68dbf..48dd1d4b129097aa5b0370bd44f0ef0028cf15f4 100644 (file)
         * 
         * @param       {string}        selector        CSS selector
         * @param       {Element=}      context         target element, assuming `document` if omitted
+        * @param       {function=}     callback        callback function pased to forEach()
         * @return      {NodeList}      matching elements
         */
-       window.elBySelAll = function(selector, context) {
-               return (context || document).querySelectorAll(selector);
+       window.elBySelAll = function(selector, context, callback) {
+               var nodeList = (context || document).querySelectorAll(selector);
+               if (typeof callback === 'function') {
+                       Array.prototype.forEach.call(nodeList, callback);
+               }
+               
+               return nodeList;
        };
        
        /**
index 89419bdd53f665b5dc030bc7331d571448a57c6d..862713a839d3f45bff531db924fe2633cb73880f 100644 (file)
        box-shadow: $parameters;
 }
 /** /deprecated */
+
+@mixin small-screen-only() {
+       /* 768px - 1px */
+       @media (max-width: 767px) {
+               @content;
+       }
+}
+
+@mixin large-screen-only() {
+       @media (min-width: 768px) {
+               @content;
+       }
+}
index f7876b3d09ded3df4e09b7b90b74591c04d8313a..fb82b29e51fa2ac2c74f1f9a2179026be30fc805 100644 (file)
@@ -1,13 +1,20 @@
 .layoutBoundary {
        margin: 0 auto;
-       padding: 0 20px;
        
-       @if $useFluidLayout {
-               min-width: $wcfLayoutMinWidth;
-               max-width: $wcfLayoutMaxWidth;
+       @include small-screen-only {
+               padding: 0 10px;
+               width: 100%;
        }
-       @else {
-               width: $wcfLayoutFixedWidth;
+       
+       @include large-screen-only {
+               padding: 0 20px;
+               
+               @if $useFluidLayout {
+                       min-width: $wcfLayoutMinWidth;
+                       max-width: $wcfLayoutMaxWidth;
+               } @else {
+                       width: $wcfLayoutFixedWidth;
+               }
        }
 }
 
index dc13ba8e8b73ab067bcbd621eeabe8975892b161..981925b502bde8322a6a649ad2b3f859fe630e08 100644 (file)
@@ -43,10 +43,6 @@ a {
        padding: 40px 0;
        z-index: 50;
        
-       > div {
-               display: flex;
-       }
-       
        a {
                color: $wcfContentLink;
                
@@ -56,35 +52,43 @@ a {
        }
 }
 
-.content {
-       flex: 1 1 auto;
-       
-       // sidebar follows
-       &:not(:last-child) {
-               flex-basis: calc(100% - 340px);
-               max-width: calc(100% - 340px); // IE fix
-       }
-}
-
-.sidebar {
-       flex: 0 0 310px;
-       
-       &:first-child {
-               margin-right: 30px;
+/* use flex-box to enforce a proper side-by-side layout on desktop */
+@include large-screen-only {
+       .main > div {
+               display: flex;
        }
        
-       & + .content {
-               flex-basis: calc(100% - 340px);
-               max-width: calc(100% - 340px); // IE fix
+       .content {
+               flex: 1 1 auto;
                
                // sidebar follows
                &:not(:last-child) {
-                       flex-basis: calc(100% - 680px);
-                       max-width: calc(100% - 680px); // IE fix
+                       flex-basis: calc(100% - 340px);
+                       max-width: calc(100% - 340px); // IE fix
+               }
+               
+               & + .sidebar {
+                       margin-left: 30px;
+               }
+       }
+       
+       .sidebar {
+               flex: 0 0 310px;
+               
+               &:first-child {
+                       margin-right: 30px;
+               }
+               
+               & + .content {
+                       flex-basis: calc(100% - 340px);
+                       max-width: calc(100% - 340px); // IE fix
+                       
+                       // sidebar follows
+                       &:not(:last-child) {
+                               flex-basis: calc(100% - 680px);
+                               max-width: calc(100% - 680px); // IE fix
+                       }
                }
        }
 }
 
-.content + .sidebar {
-       margin-left: 30px;
-}
index fc281b16607d58f4176e01bcd1df23752e1d5c49..5022206521821e51a32e81bd75ea2a43b702e234 100644 (file)
 }
 
 /* LOGO */
-#logo {
+.pageHeaderLogo {
        // use a fixed width of 50% together with the search bar to force a wrap
        flex: 0 0 50%;
        
        // gap between the two rows formed by the wrapping flex box
        margin-bottom: 15px;
        
-       > a > img.small {
+       .pageHeaderLogoSmall {
                display: none;
        }
 }
                }
        }
 }
+
+@include small-screen-only {
+       .pageHeader > div > div {
+               padding-bottom: 10px;
+               padding-top: 10px;
+       }
+       
+       .pageHeaderLogo {
+               flex: 1 1 auto;
+               margin: 0 10px;
+               order: 2;
+               text-align: center;
+               
+               .pageHeaderLogoLarge {
+                       display: none;
+               }
+               
+               .pageHeaderLogoSmall {
+                       display: inline;
+               }
+       }
+       
+       .userPanel {
+               flex: 0 0 auto;
+               order: 3;
+               
+               &::before {
+                       content: $fa-var-user;
+               }
+               
+               > .userPanelItems {
+                       display: none;
+               }
+       }
+       
+       .mainMenu {
+               flex: 0 0 auto;
+               order: 1;
+               
+               &::before {
+                       content: $fa-var-bars;
+               }
+               
+               > .boxContent {
+                       display: none;
+               }
+       }
+       
+       .mainMenu,
+       .userPanel {
+               &::before {
+                       background-color: $wcfHeaderMenuBackground;
+                       color: $wcfHeaderMenuLink;
+                       font-family: FontAwesome;
+                       font-size: 28px;
+                       line-height: 32px;
+                       padding: 5px 10px;
+               }
+               
+               &:hover::before {
+                       background-color: $wcfHeaderMenuBackgroundActive;
+                       color: $wcfHeaderMenuLinkActive;
+               }
+       }
+       
+       .pageHeaderSearch {
+               display: none;
+       }
+}
index 06aeb8ae7c6fe36ab81cc8421342f733cf0a60ed..97e62ba7aba473ba1887a8bfe74963b3e1b89df5 100644 (file)
                }
        }
        
-       #logo {
-               flex: 0 auto;
-               margin-bottom: 0;
-               order: 1;
-               
-               > a > .large {
-                       display: none;
-               }
-               
-               > a > .small {
-                       display: block;
-               }
-       }
-       
-       .mainMenu {
-               flex: 1 auto;
-               order: 2;
-               margin: 0 20px;
-       }
-       
-       .userPanel {
-               flex: 0 auto;
-               order: 3;
-               margin-right: 20px;
-       }
-       
-       .pageHeaderSearch {
-               flex: 0 auto;
-               order: 4;
-       }
-       
        .pageHeaderSearchInputContainer:not(.open) {
                > .pageHeaderSearchInput {
                        padding-right: 20px;
                        pointer-events: none;
                }
        }
+       
+       @include large-screen-only {
+               #logo {
+                       flex: 0 auto;
+                       margin-bottom: 0;
+                       order: 1;
+                       
+                       > a > .large {
+                               display: none;
+                       }
+                       
+                       > a > .small {
+                               display: block;
+                       }
+               }
+               
+               .mainMenu {
+                       flex: 1 auto;
+                       order: 2;
+                       margin: 0 20px;
+               }
+               
+               .userPanel {
+                       flex: 0 auto;
+                       order: 3;
+                       margin-right: 20px;
+               }
+               
+               .pageHeaderSearch {
+                       flex: 0 auto;
+                       order: 4;
+               }
+       }
 }
index 48388cd65132fe1d20f279161b6348969343f8e9..0c8fafa021eeccb731fd277cad20982ccee270c1 100644 (file)
@@ -1,50 +1,58 @@
-.pageNavigation {
-       background-color: $wcfNavigationBackground;
-       color: $wcfNavigationText;
-       flex: 0 0 auto;
-       padding: 5px 0;
-       z-index: 25;
-       
-       > div {
-               align-items: center;
-               display: flex;
-               justify-content: flex-end;
-               height: 30px;
-       }
-       
-       .icon {
+@include large-screen-only {
+       .pageNavigation {
+               background-color: $wcfNavigationBackground;
                color: $wcfNavigationText;
-       }
-       
-       a {
-               color: $wcfNavigationLink;
+               flex: 0 0 auto;
+               padding: 5px 0;
+               z-index: 25;
+               
+               > div {
+                       align-items: center;
+                       display: flex;
+                       justify-content: flex-end;
+                       height: 30px;
+               }
+               
+               .icon {
+                       color: $wcfNavigationText;
+               }
                
-               &:hover {
-                       color: $wcfNavigationLinkActive;
+               a {
+                       color: $wcfNavigationLink;
+                       
+                       &:hover {
+                               color: $wcfNavigationLinkActive;
+                       }
                }
        }
-}
-
-.pageNavigationIcons {
-       display: flex;
-       flex: 0 0 auto;
-       flex-direction: row-reverse;
        
-       > li {
+       .pageNavigationIcons {
+               display: flex;
                flex: 0 0 auto;
+               flex-direction: row-reverse;
                
-               &:not(:last-child) {
-                       margin-left: 10px;
-               }
-               
-               > a {
-                       > .icon {
-                               color: $wcfHeaderLink;
+               > li {
+                       flex: 0 0 auto;
+                       
+                       &:not(:last-child) {
+                               margin-left: 10px;
                        }
                        
-                       &:hover > .icon {
-                               color: $wcfHeaderLinkActive;
+                       > a {
+                               > .icon {
+                                       color: $wcfHeaderLink;
+                               }
+                               
+                               &:hover > .icon {
+                                       color: $wcfHeaderLinkActive;
+                               }
                        }
                }
        }
 }
+
+@include small-screen-only {
+       .pageNavigation {
+               display: none;
+       }
+}
index b0c125bb1cdf6d11423efd67d95ac5b4048a42b2..7fd26cc8d8f44c26a6dfb45524b74b5ca6375e8b 100644 (file)
        }
 }
 
-@media only screen and (max-width: 800px) {
-}
-
 /* static dialogs */
 .jsStaticDialogContent {
        display: none;
index bdedf7f9f9fd05b6d705cb1da33d9398e593db31..452e26318d5c2a9926530e8b2ff4cb95266a2525 100644 (file)
        min-height: 20px;
 }
 
-@media only screen and (max-width: 800px) {
+@include small-screen-only {
        .dropdownMenu {
                left: 0 !important;
                right: 0 !important;
index 7fd65205a176e3cdbe41f5ac9a4f9807c74c8d37..4b8b60ccb2332923307bf31caf40e68642d9cc2c 100644 (file)
                overflow: visible;
                max-height: none;
        }
+       
+       @include small-screen-only {
+               > .elementPointer {
+                       display: none;
+               }
+       }
 }
 
 /* drop down header */
                flex: 0 0 auto;
                margin-left: 5px;
        }
+       
+       @include small-screen-only {
+               padding: 10px;
+       }
 }
 
 /* container for dropdown items */
 .interactiveDropdownItemsContainer {
        border: 1px solid $wcfContentBorderInner;
        border-width: 1px 0;
-       max-height: 300px;
        
        &.ps-container {
                > .interactiveDropdownItems {
                        align-items: center;
                        overflow: hidden;
                }
+               
+               @include small-screen-only {
+                       padding: 10px;
+               }
        }
        
        .loading,
        }
 }
 
-@media only screen and (min-width: 801px) {
+@include large-screen-only {
        .interactiveDropdown {
                min-width: 350px;
        }
        
        .interactiveDropdownItemsContainer {
+               max-height: 400px;
                overflow: hidden;
                position: relative;
        }
        }
 }
 
-/* todo: mobile version
-@media only screen and (max-width: 800px) {
-       .DEBUG_ONLY_interactiveDropdown {
-               border-width: 1px 0;
-               box-sizing: border-box;
-               left: 0 !important;
-               right: 0 !important;
-               width: 100%;
+@include small-screen-only {
+       .interactiveDropdown {
+               bottom: 0;
+               display: flex;
+               flex-direction: column;
+               left: 0;
+               position: fixed;
+               right: 0;
+       }
+       
+       .interactiveDropdownHeader {
+               flex: 0 0 auto;
+       }
+       
+       .interactiveDropdownItemsContainer {
+               flex: 1 1 auto;
+               overflow: auto;
                
-               > .interactiveDropdownItemsContainer {
-                       overflow-x: auto;
+               /* increase the clickable area of the mark as read icon */
+               .interactiveDropdownItemMarkAsRead {
+                       bottom: 0;
+                       position: absolute;
+                       right: 0;
+                       top: 0;
+                       width: 36px; /* 16px icon + 2x 10px padding */
                        
-                       > .interactiveDropdownItems > li.interactiveDropdownItemOutstandingIcon > div.interactiveDropdownItemMarkAsRead {
-                               bottom: 0;
-                               position: absolute;
-                               right: 0;
-                               top: 0;
-                               //width: (@wcfGapSmall + @wcfGapTiny) + 16px + (@wcfGapSmall + @wcfGapTiny);
+                       > a {
+                               display: block;
+                               height: 100%;
+                               text-align: center;
                                
-                               > a {
-                                       display: block;
-                                       height: 100%;
-                                       text-align: center;
+                               > .icon {
+                                       position: relative;
+                                       top: 50%;
                                        
-                                       > .icon {
-                                               position: relative;
-                                               top: 50%;
-                                               
-                                               transform: translateY(-50%);
-                                               -ms-transform: translateY(-50%);
-                                               -webkit-transform: translateY(-50%);
-                                       }
+                                       transform: translateY(-50%);
                                }
                        }
                }
        }
+       
+       .interactiveDropdownShowAll {
+               flex: 0 0 auto;
+       }
 }
-*/
\ No newline at end of file
diff --git a/wcfsetup/install/files/style/ui/menuMobile.scss b/wcfsetup/install/files/style/ui/menuMobile.scss
new file mode 100644 (file)
index 0000000..5615ef4
--- /dev/null
@@ -0,0 +1,232 @@
+/* animations for overlay appearing from the left */
+@keyframes wcfMenuOverlayLeft {
+       0%   { visibility: hidden;  transform: translateX(-100%); }
+       100% { visibility: visible; transform: translateX(0);     }
+}
+
+@keyframes wcfMenuOverlayLeftOut {
+       0%   { visibility: visible; transform: translateX(0);     }
+       99%  { visibility: visible; transform: translateX(-100%); }
+       100% { visibility: hidden;  transform: translateX(-100%); }
+}
+
+/* animations for overlay appearing from the right */
+@keyframes wcfMenuOverlayRight {
+       0%   { visibility: hidden;  transform: translateX(100%); }
+       100% { visibility: visible; transform: translateX(0);    }
+}
+
+@keyframes wcfMenuOverlayRightOut {
+       0%   { visibility: visible; transform: translateX(0);    }
+       99%  { visibility: visible; transform: translateX(100%); }
+       100% { visibility: hidden;  transform: translateX(100%); }
+}
+
+/* menu container */
+.menuOverlayMobile {
+       bottom: 0;
+       left: 0;
+       overflow: hidden;
+       position: fixed;
+       right: 0;
+       top: 0;
+       visibility: hidden;
+       z-index: 320;
+       
+       &.enableAnimation {
+               animation: wcfMenuOverlayLeftOut .3s;
+               animation-fill-mode: forwards;
+               
+               /* different animation for user menu */
+               &.pageUserMenuMobile {
+                       animation-name: wcfMenuOverlayRightOut;
+               }
+       }
+       
+       &.open {
+               animation: wcfMenuOverlayLeft .3s;
+               animation-fill-mode: forwards;
+               
+               /* different animation for user menu */
+               &.pageUserMenuMobile {
+                       animation-name: wcfMenuOverlayRight;
+               }
+       }
+       
+       /* work-around to avoid setting explicit visibility */
+       > .menuOverlayItemList:not(.hidden) {
+               visibility: inherit;
+       }
+}
+
+.menuOverlayItemWrapper {
+       display: flex;
+       justify-content: flex-end;
+       
+       > .menuOverlayItemLink {
+               flex: 1 1 auto;
+       }
+}
+
+.menuOverlayItemList {
+       background-color: rgb(44, 62, 80);
+       box-shadow: -5px 0 10px 0 rgba(0, 0, 0, .2);
+       list-style-type: none;
+       margin: 0;
+       padding: 10px 0;
+       position: fixed;
+       top: 0;
+       left: -1px;
+       right: 0;
+       bottom: 0;
+       width: calc(100% + 1px);
+       z-index: 450;
+       transition: margin-left .3s cubic-bezier(.25, .46, .45, .94);
+       transition-timing-function: linear;
+       will-change: margin-left;
+       
+       /* chaining `.hidden` and `.active` below is required because each active
+          item list receives `.active` but it could still be `.hidden` due to
+          a child list being active */
+       &.hidden,
+       &.hidden.active {
+               margin-left: -25%;
+       }
+}
+
+.menuOverlayItemSpacer {
+       margin-top: 20px;
+       
+       /* avoid successive spacers piling up */
+       & + .menuOverlayItemSpacer {
+               display: none;
+       }
+}
+
+.menuOverlayItem {
+       &:not(:last-child) {
+               margin-bottom: 1px
+       }
+       
+       /* nested item list */
+       > .menuOverlayItemList {
+               margin-left: 110%;
+               z-index: 500;
+               
+               &.active {
+                       margin-left: 0;
+                       overflow: scroll;
+               }
+       }
+}
+
+.menuOverlayItemLink,
+.menuOverlayTitle,
+.menuOverlayBackLink {
+       color: rgb(255, 255, 255);
+       display: block;
+       font-size: 14px;
+       padding: 10px 30px;
+       position: relative;
+}
+
+.menuOverlayItemLink {
+       background-color: rgb(52, 73, 94);
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+       
+       .icon::before {
+               color: #fff;
+       }
+       
+       /* wrapper class for links containing an additional badge */
+       &.menuOverlayItemBadge {
+               align-items: center;
+               display: flex;
+               padding-right: 10px;
+               
+               /* different padding if there is no additional icon after the link,
+                  ensures proper alignment for links with badges containing a child
+                  item list */
+               &:last-child {
+                       /* 55px = 10px padding + 1px margin + icon */
+                       /* icon = 2x 10px padding + 16px width */
+                       padding-right: 55px;
+               }
+               
+               > .menuOverlayItemTitle {
+                       flex: 1 1 auto;
+                       overflow: hidden;
+                       text-overflow: ellipsis;
+               }
+               
+               > .badge {
+                       flex: 0 0 auto;
+               }
+       }
+       
+       &.menuOverlayItemLinkMore::after {
+               color: rgb(204, 204, 204);
+               content: $fa-var-angle-right;
+               display: block;
+               font-family: FontAwesome;
+               font-size: 24px;
+               position: absolute;
+               right: 10px;
+               top: 50%;
+               transform: translateY(-50%);
+       }
+}
+
+.menuOverlayTitle {
+       color: rgb(204, 204, 204);
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+       
+       &:not(:first-child) {
+               margin-top: 10px;
+       }
+}
+
+/* icon link sharing the space with a link or (header only) the logo */
+.menuOverlayItemLinkIcon {
+       background-color: rgb(52, 73, 94);
+       flex: 0 0 auto;
+       margin-left: 1px;
+       padding: 10px;
+       
+       /* force explicit dimensions because no each .icon24 is of equal height/width */
+       height: 44px;
+       width: 44px;
+       
+       > .icon::before {
+               color: #fff;
+       }
+}
+
+.menuOverlayBackLink::before {
+       color: rgb(204, 204, 204);
+       content: $fa-var-angle-left;
+       display: block;
+       font-family: FontAwesome;
+       font-size: 18px;
+       position: absolute;
+       left: 10px;
+       top: 50%;
+       transform: translateY(-50%);
+}
+
+.menuOverlayLogoWrapper {
+       flex: 1 1 auto;
+       padding: 5px;
+       display: flex;
+       
+       .menuOverlayLogo {
+               flex: 1 1 auto;
+               background-size: contain;
+               background-repeat: no-repeat;
+               background-position: center;
+       }
+}
index 438f0c0735a86b21be6b1a03315b2446b54e43f6..387365f86026ea0eef29a7a6f7eee7fd542a76f3 100644 (file)
 }
 
 /* disable auto zoom in mobile safari */
-@media only screen and (max-width: 800px) {
+@include small-screen-only {
        .redactor-editor + textarea {
                font-size: 16px;
                max-height: 500px;
index cbd6d08bfbb55e66ce813e39ea8f9deb7fafecfc..c3271334387423af7c295074450c9a4268bbbf65 100644 (file)
        }
 }
 
-@media only screen and (max-width: 800px) {
+@include small-screen-only {
        .messageTabMenu {
                > nav > ul > li:not(.active) > a {
                        > span.icon {