Implemented the flexible menu
authorAlexander Ebert <ebert@woltlab.com>
Mon, 4 May 2015 11:57:12 +0000 (13:57 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 4 May 2015 11:57:38 +0000 (13:57 +0200)
com.woltlab.wcf/templates/headIncludeJavaScript.tpl
wcfsetup/install/files/js/WCF.js
wcfsetup/install/files/js/WoltLab/WCF/Bootstrap.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/UI/FlexibleMenu.js [new file with mode: 0644]

index 02685ae5d0bebb927ce0dd23aaf944aa8e090f6a..e63e9235dbd6c3e67afb86e7c919d635e0a3b71a 100644 (file)
                
                {event name='javascriptLanguageImport'}
        });
-       
-       $(function() {
-       console.time('wcf');
-       //WCF.TabMenu.init();
-       //WCF.System.FlexibleMenu.init();
-       console.timeEnd('wcf');
-       });
 </script>
 
 <script data-relocate="true" src="{@$__wcf->getPath()}js/require.config.js"></script>
                baseUrl: '{@$__wcf->getPath()}js'
        });
        
-       /*require(function(require) {
-               var ui = require('WCF/UI');
-               
-               console.debug(ui);
-       });*/
-       
        define('jQuery', [], function() { return window.jQuery; });
        
        $.holdReady(true);
        require(['WoltLab/WCF/Bootstrap'], function(bootstrap) {
                bootstrap.setup();
        });
-       /*
-       require(['WoltLab/WCF/Date/Time/Relative', 'UI/SimpleDropdown'], function(relative, dropdown) {
-               relative.init();
-               
-               console.time('wcfNew');
-               dropdown.setup();
-               console.timeEnd('wcfNew');
-               
-               $.holdReady(false);
-       });
-       */
-       /*
-       require(function(require) {
-               var core = require('WoltLab/WCF/Core');
-               
-               core.Init();
-       });
-       
-       require(['WoltLab/WCF/Core'], function(core) {
-               core.Init();
-       });*/
 </script>
 
 {if ENABLE_DEBUG_MODE}
index a132200bde2e07dbce4779aa6bc85a39303cbeed..875b0a8c86a5a87810755e0e31e05e2e2d3811f8 100755 (executable)
@@ -3877,8 +3877,8 @@ WCF.TabMenu = {
         * Initializes all TabMenus
         */
        init: function() {
-               require(['WoltLab/WCF/UI/TabMenu'], function(tabMenu) {
-                       tabMenu.init();
+               require(['WoltLab/WCF/UI/TabMenu'], function(UITabMenu) {
+                       UITabMenu.setup();
                });
                
                return;
@@ -6647,74 +6647,10 @@ WCF.System.Dependency.Manager = {
  * Provides flexible dropdowns for tab-based menus.
  */
 WCF.System.FlexibleMenu = {
-       /**
-        * list of containers
-        * @var object<jQuery>
-        */
-       _containers: { },
-       
-       /**
-        * list of registered container ids
-        * @var array<string>
-        */
-       _containerIDs: [ ],
-       
-       /**
-        * list of dropdowns
-        * @var object<jQuery>
-        */
-       _dropdowns: { },
-       
-       /**
-        * list of dropdown menus
-        * @var object<jQuery>
-        */
-       _dropdownMenus: { },
-       
-       /**
-        * list of hidden status for containers
-        * @var object<boolean>
-        */
-       _hasHiddenItems: { },
-       
-       /**
-        * true if menus are currently rebuilt
-        * @var boolean
-        */
-       _isWorking: false,
-       
-       /**
-        * list of tab menu items per container
-        * @var object<jQuery>
-        */
-       _menuItems: { },
-       
        /**
         * Initializes the WCF.System.FlexibleMenu class.
         */
-       init: function() {
-               // register .mainMenu and .navigationHeader by default
-               this.registerMenu('mainMenu');
-               this.registerMenu($('.navigationHeader:eq(0)').wcfIdentify());
-               
-               this._registerTabMenus();
-               
-               $(window).resize($.proxy(this.rebuildAll, this));
-               WCF.DOMNodeInsertedHandler.addCallback('WCF.System.FlexibleMenu', $.proxy(this._registerTabMenus, this));
-       },
-       
-       /**
-        * Registers tab menus.
-        */
-       _registerTabMenus: function() {
-               // register tab menus
-               $('.tabMenuContainer:not(.jsFlexibleMenuEnabled), .messageTabMenu:not(.jsFlexibleMenuEnabled)').each(function(index, tabMenuContainer) {
-                       var $navigation = $(tabMenuContainer).addClass('jsFlexibleMenuEnabled').children('nav');
-                       if ($navigation.length && $navigation.find('> ul:eq(0) > li').length) {
-                               WCF.System.FlexibleMenu.registerMenu($navigation.wcfIdentify());
-                       }
-               });
-       },
+       init: function() { /* does nothing */ },
        
        /**
         * Registers a tab-based menu by id.
@@ -6732,39 +6668,9 @@ WCF.System.FlexibleMenu = {
         * @param       string          containerID
         */
        registerMenu: function(containerID) {
-               var $container = $('#' + containerID);
-               if (!$container.length) {
-                       console.debug("[WCF.System.FlexibleMenu] Unable to find container identified by '" + containerID + "', aborting.");
-                       return;
-               }
-               
-               this._containerIDs.push(containerID);
-               this._containers[containerID] = $container;
-               this._menuItems[containerID] = $container.find('> ul:eq(0) > li');
-               this._dropdowns[containerID] = $('<li class="dropdown jsFlexibleMenuDropdown"><a class="icon icon16 icon-list" /></li>').data('containerID', containerID).click($.proxy(this._click, this));
-               this._dropdownMenus[containerID] = $('<ul class="dropdownMenu" />').appendTo(this._dropdowns[containerID]);
-               this._hasHiddenItems[containerID] = false;
-               
-               this.rebuild(containerID);
-               
-               WCF.Dropdown.initDropdown(this._dropdowns[containerID].children('a'));
-       },
-       
-       /**
-        * Rebuilds all registered containers.
-        */
-       rebuildAll: function() {
-               if (this._isWorking) {
-                       return;
-               }
-               
-               this._isWorking = true;
-               
-               for (var $i = 0, $length = this._containerIDs.length; $i < $length; $i++) {
-                       this.rebuild(this._containerIDs[$i]);
-               }
-               
-               this._isWorking = false;
+               require(['WoltLab/WCF/UI/FlexibleMenu'], function(UIFlexibleMenu) {
+                       UIFlexibleMenu.register(containerID);
+               });
        },
        
        /**
@@ -6773,111 +6679,9 @@ WCF.System.FlexibleMenu = {
         * @param       string          containerID
         */
        rebuild: function(containerID) {
-               if (!this._containers[containerID]) {
-                       console.debug("[WCF.System.FlexibleMenu] Cannot rebuild unknown container identified by '" + containerID + "'");
-                       return;
-               }
-               
-               var $container = this._containers[containerID];
-               
-               // hide all items
-               var $menuItems = this._menuItems[containerID].hide();
-               
-               // the active item must always be visible
-               var $activeItem = $menuItems.filter('.active, .ui-state-active').show();
-               
-               // insert dropdown for calculation purposes
-               if (!this._hasHiddenItems[containerID]) {
-                       this._dropdowns[containerID].appendTo($container.children('ul:eq(0)'));
-               }
-               var $dropdownWidth = this._dropdowns[containerID].outerWidth(true);
-               
-               // get maximum width
-               var $parent = $container.parent();
-               var $maximumWidth = $parent.innerWidth();
-               
-               // exclude padding
-               $maximumWidth -= $parent.cssAsNumber('padding-left') + $parent.cssAsNumber('padding-right');
-               
-               // substract margins and paddings from the container itself
-               $maximumWidth -= $container.cssAsNumber('margin-left') + $container.cssAsNumber('margin-right');
-               $maximumWidth -= $container.cssAsNumber('padding-left') + $container.cssAsNumber('padding-right');
-               
-               // substract paddings from the actual list
-               $maximumWidth -= $container.children('ul:eq(0)').cssAsNumber('padding-left') + $container.children('ul:eq(0)').cssAsNumber('padding-right');
-               
-               // the active item must always be visible, substract its width
-               $maximumWidth -= $activeItem.outerWidth(true);
-               
-               // show items until maximum width is exceeded
-               this._hasHiddenItems[containerID] = false;
-               for (var $i = 0; $i < $menuItems.length; $i++) {
-                       var $item = $($menuItems[$i]);
-                       
-                       // ignore active item because it is already visible
-                       if ($item.hasClass('active') || $item.hasClass('ui-state-active')) {
-                               continue;
-                       }
-                       
-                       var $width = $item.outerWidth(true);
-                       if ($maximumWidth - $width > 0) {
-                               $maximumWidth -= $width;
-                               $item.show();
-                       }
-                       else {
-                               // check if dropdown no longer fits in
-                               if ($maximumWidth < $dropdownWidth) {
-                                       // hide previous item to clear up some space for the dropdown unless it is the active item
-                                       var $prev = $item.prev();
-                                       if ($prev.hasClass('active') || $prev.hasClass('ui-state-active')) {
-                                               $prev.prev().hide();
-                                       }
-                                       else {
-                                               $prev.hide();
-                                       }
-                               }
-                               
-                               this._hasHiddenItems[containerID] = true;
-                               
-                               break;
-                       }
-               }
-               
-               // rebuild dropdown
-               if (this._hasHiddenItems[containerID]) {
-                       this._dropdownMenus[containerID].empty();
-                       var self = this;
-                       $menuItems.each($.proxy(function(index, item) {
-                               if ($(item).is(':visible')) {
-                                       return true;
-                               }
-                               
-                               $('<li>' + $(item).html() + '</li>').data('index', index).appendTo(this._dropdownMenus[containerID]).click(function(event) {
-                                       // forward click to the original item
-                                       var $item = $($menuItems[$(event.currentTarget).data('index')]);
-                                       if ($item[0].tagName === 'A') {
-                                               $item.trigger('click');
-                                       }
-                                       else if ($item[0].tagName === 'LI') {
-                                               $item.find('a').trigger('click');
-                                       }
-                                       
-                                       // prevent links being followed (they are mandatory in jQuery UI's tab menu)
-                                       if ($item.parent().hasClass('ui-tabs-nav')) {
-                                               event.preventDefault();
-                                       }
-                                       
-                                       // force a rebuild to guarantee the active item being visible
-                                       setTimeout(function() {
-                                               self.rebuild(containerID);
-                                       }, 50);
-                               });
-                       }, this));
-               }
-               else {
-                       // remove dropdown if there are no hidden items
-                       this._dropdowns[containerID].detach();
-               }
+               require(['WoltLab/WCF/UI/FlexibleMenu'], function(UIFlexibleMenu) {
+                       UIFlexibleMenu.rebuild(containerID);
+               });
        }
 };
 
index fa7c33c8bd6048ca4932c24e0db32a37a819fbac..9ceafe0b3394165e9967e2fb01e7dc9c1fc6e4ba 100644 (file)
@@ -9,9 +9,11 @@
  * @module     WoltLab/WCF/Bootstrap
  */
 define(
-       [       'favico', 'enquire', 'WoltLab/WCF/Date/Time/Relative', 'UI/SimpleDropdown', 'WoltLab/WCF/UI/Mobile', 'WoltLab/WCF/UI/TabMenu'], 
-       function(favico,   enquire,   relativeTime,                     simpleDropdown,      uiMobile,                TabMenu)
+       [       'favico', 'enquire', 'WoltLab/WCF/Date/Time/Relative', 'UI/SimpleDropdown', 'WoltLab/WCF/UI/Mobile', 'WoltLab/WCF/UI/TabMenu', 'WoltLab/WCF/UI/FlexibleMenu'], 
+       function(favico,   enquire,   relativeTime,                     simpleDropdown,      uiMobile,                TabMenu,                  FlexibleMenu)
 {
+       "use strict";
+       
        window.Favico = favico;
        window.enquire = enquire;
        
@@ -28,10 +30,11 @@ define(
                        simpleDropdown.setup();
                        uiMobile.setup();
                        TabMenu.setup();
+                       FlexibleMenu.setup();
                        
                        $.holdReady(false);
                }
-       }
+       };
        
        return new Bootstrap();
 });
index 2b73ea95bef01e16d400855f4d3be85a607f689e..d6fdea6a18ec52da71265ea53c944811765db32f 100644 (file)
@@ -21,6 +21,18 @@ define(['DOM/Util'], function(DOMUtil) {
                function(el, tagName) { return el.nodeName === tagName; }
        ];
        
+       var _children = function(el, type, value) {
+               var children = [];
+               
+               for (var i = 0; i < el.childElementCount; i++) {
+                       if (_probe[type](el.children[i], value)) {
+                               children.push(el.children[i]);
+                       }
+               }
+               
+               return children;
+       };
+       
        var _parent = function(el, type, value) {
                el = el.parentNode;
                
@@ -50,6 +62,39 @@ define(['DOM/Util'], function(DOMUtil) {
         */
        function DOMTraverse() {};
        DOMTraverse.prototype = {
+               /**
+                * Examines child elements and returns all children matching the given selector.
+                * 
+                * @param       {Element}               el              element
+                * @param       {string}                selector        CSS selector to match child elements against
+                * @return      {array<Element>}        list of children matching the selector
+                */
+               childrenBySel: function(el, selector) {
+                       return _children(el, SELECTOR, selector);
+               },
+               
+               /**
+                * Examines child elements and returns all children that have the given CSS class set.
+                * 
+                * @param       {Element}               el              element
+                * @param       {string}                className       CSS class name
+                * @return      {array<Element>}        list of children with the given class
+                */
+               childrenByClass: function(el, className) {
+                       return _children(el, CLASS_NAME, className);
+               },
+               
+               /**
+                * Examines child elements and returns all children which equal the given tag.
+                * 
+                * @param       {Element}               el              element
+                * @param       {string}                tagName         element tag name
+                * @return      {array<Element>}        list of children equaling the tag name
+                */
+               childrenByTag: function(el, tagName) {
+                       return _children(el, TAG_NAME, tagName);
+               },
+               
                /**
                 * Examines parent nodes and returns the first parent that matches the given selector.
                 * 
index 12f5783516e7dde8586b732e3d91b5d87e2e95ef..2a5a7e845463f6f13ec3974129d554ea6bc8f544 100644 (file)
@@ -132,14 +132,14 @@ define(function() {
                        return {
                                top: rect.top + document.body.scrollTop,
                                left: rect.left + document.body.scrollLeft
-                       }
+                       };
                },
                
                /**
                 * Applies a list of CSS properties to an element.
                 * 
                 * @param       {Element}               el      element
-                * @param       {Object<string, mixed styles  list of CSS styles
+                * @param       {Object<string, mixed>} styles  list of CSS styles
                 */
                setStyles: function(el, styles) {
                        for (var property in styles) {
@@ -147,6 +147,25 @@ define(function() {
                                        el.style.setProperty(property, styles[property]);
                                }
                        }
+               },
+               
+               /**
+                * Returns a style property value as integer.
+                * 
+                * The behavior of this method is undefined for properties that are not considered
+                * to have a "numeric" value, e.g. "background-image".
+                * 
+                * @param       {CSSStyleDeclaration}   styles          result of window.getComputedStyle()
+                * @param       {string}                propertyName    property name
+                * @return      {integer}       property value as integer
+                */
+               styleAsInt: function(styles, propertyName) {
+                       var value = styles.getPropertyValue(propertyName);
+                       if (value === null) {
+                               return 0;
+                       }
+                       
+                       return parseInt(value);
                }
        };
        
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/FlexibleMenu.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/FlexibleMenu.js
new file mode 100644 (file)
index 0000000..9159613
--- /dev/null
@@ -0,0 +1,202 @@
+/**
+ * Dynamically transforms menu-like structures to handle items exceeding the available width
+ * by moving them into a separate dropdown.  
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/UI/FlexibleMenu
+ */
+define(['Core', 'Dictionary', 'DOM/Traverse', 'DOM/Util', 'UI/SimpleDropdown'], function(Core, Dictionary, DOMTraverse, DOMUtil, SimpleDropdown) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       var UIFlexibleMenu = function() {
+               this._containers = new Dictionary();
+               this._dropdowns = new Dictionary();
+               this._dropdownMenus = new Dictionary();
+               this._itemLists = new Dictionary();
+       };
+       UIFlexibleMenu.prototype = {
+               /**
+                * Register default menus and set up event listeners.
+                */
+               setup: function() {
+                       if (document.getElementById('mainMenu') !== null) this.register('mainMenu');
+                       var navigationHeader = document.querySelector('.navigationHeader');
+                       if (navigationHeader !== null) this.register(DOMUtil.identify(navigationHeader));
+                       
+                       
+                       window.addEventListener('resize', this.rebuildAll.bind(this));
+                       WCF.DOMNodeInsertedHandler.addCallback('WoltLab/WCF/UI/FlexibleMenu', this.registerTabMenus.bind(this));
+               },
+               
+               /**
+                * Registers a menu by element id.
+                * 
+                * @param       {string}        containerId     element id
+                */
+               register: function(containerId) {
+                       var container = document.getElementById(containerId);
+                       if (container === null) {
+                               throw "Expected a valid element id, '" + containerId + "' does not exist.";
+                       }
+                       
+                       if (this._containers.has(containerId)) {
+                               return;
+                       }
+                       
+                       var lists = DOMTraverse.childrenByTag(container, 'UL');
+                       if (!lists.length) {
+                               throw "Expected an <ul> element as child of container '" + containerId + "'.";
+                       }
+                       
+                       this._containers.set(containerId, container);
+                       this._itemLists.set(containerId, lists[0]);
+                       
+                       this.rebuild(containerId);
+               },
+               
+               /**
+                * Registers tab menus.
+                */
+               registerTabMenus: function() {
+                       var tabMenus = document.querySelectorAll('.tabMenuContainer:not(.jsFlexibleMenuEnabled), .messageTabMenu:not(.jsFlexibleMenuEnabled)');
+                       for (var i = 0, length = tabMenus.length; i < length; i++) {
+                               var tabMenu = tabMenus[i];
+                               var navs = DOMTraverse.childrenByTag(tabMenu, 'NAV');
+                               if (navs.length !== 0) {
+                                       tabMenu.classList.add('jsFlexibleMenuEnabled');
+                                       this.register(DOMUtil.identify(navs[0]));
+                               }
+                       }
+               },
+               
+               /**
+                * Rebuilds all menus, e.g. on window resize.
+                */
+               rebuildAll: function() {
+                       this._containers.forEach((function(container, containerId) {
+                               this.rebuild(containerId);
+                       }).bind(this));
+               },
+               
+               /**
+                * Rebuild the menu identified by given element id.
+                * 
+                * @param       {string}        containerId     element id
+                */
+               rebuild: function(containerId) {
+                       var container = this._containers.get(containerId);
+                       if (container === null) {
+                               throw "Expected a valid element id, '" + containerId + "' is unknown.";
+                       }
+                       
+                       var styles = window.getComputedStyle(container);
+                       
+                       var availableWidth = container.parentNode.clientWidth;
+                       availableWidth -= DOMUtil.styleAsInt(styles, 'margin-left');
+                       availableWidth -= DOMUtil.styleAsInt(styles, 'margin-right');
+                       
+                       var list = this._itemLists.get(containerId);
+                       var items = DOMTraverse.childrenByTag(list, 'LI');
+                       var dropdown = this._dropdowns.get(containerId);
+                       var dropdownWidth = 0;
+                       if (dropdown !== null) {
+                               // show all items for calculation
+                               for (var i = 0, length = items.length; i < length; i++) {
+                                       var item = items[i];
+                                       if (item.classList.contains('dropdown')) {
+                                               continue;
+                                       }
+                                       
+                                       item.style.removeProperty('display'); 
+                               }
+                               
+                               if (dropdown.parentNode !== null) {
+                                       dropdownWidth = DOMUtil.outerWidth(dropdown);
+                               }
+                       }
+                       
+                       var currentWidth = list.scrollWidth - dropdownWidth;
+                       var hiddenItems = [];
+                       if (currentWidth > availableWidth) {
+                               // hide items starting with the last one
+                               for (var i = items.length - 1; i >= 0; i--) {
+                                       var item = items[i];
+                                       
+                                       // ignore dropdown and active item
+                                       if (item.classList.contains('dropdown') || item.classList.contains('active') || item.classList.contains('ui-state-active')) {
+                                               continue;
+                                       }
+                                       
+                                       hiddenItems.push(item);
+                                       item.style.setProperty('display', 'none');
+                                       
+                                       if (list.scrollWidth < availableWidth) {
+                                               break;
+                                       }
+                               }
+                       }
+                       
+                       if (hiddenItems.length) {
+                               var dropdownMenu;
+                               if (dropdown === null) {
+                                       dropdown = document.createElement('li');
+                                       dropdown.className = 'dropdown jsFlexibleMenuDropdown';
+                                       var icon = document.createElement('a');
+                                       icon.className = 'icon icon16 fa-list';
+                                       dropdown.appendChild(icon);
+                                       
+                                       dropdownMenu = document.createElement('ul');
+                                       dropdownMenu.classList.add('dropdownMenu');
+                                       dropdown.appendChild(dropdownMenu);
+                                       
+                                       this._dropdowns.set(containerId, dropdown);
+                                       this._dropdownMenus.set(containerId, dropdownMenu);
+                                       
+                                       SimpleDropdown.init(icon);
+                               }
+                               else {
+                                       dropdownMenu = this._dropdownMenus.get(containerId);
+                               }
+                               
+                               if (dropdown.parentNode === null) {
+                                       list.appendChild(dropdown);
+                               }
+                               
+                               // build dropdown menu
+                               var fragment = document.createDocumentFragment();
+                               
+                               var self = this;
+                               hiddenItems.forEach(function(hiddenItem) {
+                                       var item = document.createElement('li');
+                                       item.innerHTML = hiddenItem.innerHTML;
+                                       
+                                       item.addEventListener('click', (function(event) {
+                                               event.preventDefault();
+                                               
+                                               Core.triggerEvent(hiddenItem.querySelector('a'), 'click');
+                                               
+                                               // force a rebuild to guarantee the active item being visible
+                                               setTimeout(function() {
+                                                       self.rebuild(containerId);
+                                               }, 59);
+                                       }).bind(this));
+                                       
+                                       fragment.appendChild(item);
+                               });
+                               
+                               dropdownMenu.innerHTML = '';
+                               dropdownMenu.appendChild(fragment);
+                       }
+                       else if (dropdown !== null && dropdown.parentNode !== null) {
+                               dropdown.parentNode.removeChild(dropdown);
+                       }
+               }
+       };
+       
+       return new UIFlexibleMenu();
+});
\ No newline at end of file