Added reusable dropdown menus
authorAlexander Ebert <ebert@woltlab.com>
Mon, 16 Nov 2015 23:51:17 +0000 (00:51 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 16 Nov 2015 23:51:17 +0000 (00:51 +0100)
Dropdown menus are usually tied to a specific element, but certain
scenarios require only a single menu that can be used for multiple
source elements. This decreases the impact on the page DOM and allows
for easier management in these cases.

wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Reusable.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Simple.js
wcfsetup/install/files/js/require.config.js

diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Reusable.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Reusable.js
new file mode 100644 (file)
index 0000000..b0acdc2
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * Simple interface to work with reusable dropdowns that are not bound to a specific item.
+ * 
+ * @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/Dropdown/Reusable
+ */
+define(['Dictionary', 'Ui/SimpleDropdown'], function(Dictionary, UiSimpleDropdown) {
+       "use strict";
+       
+       var _dropdowns = new Dictionary();
+       var _ghostElementId = 0;
+       
+       /**
+        * Returns dropdown name by internal identifier.
+        *
+        * @param       {string}        identifier      internal identifier
+        * @returns     {string}        dropdown name
+        */
+       function _getDropdownName(identifier) {
+               if (!_dropdowns.has(identifier)) {
+                       throw new Error("Unknown dropdown identifier '" + identifier + "'");
+               }
+               
+               return _dropdowns.get(identifier);
+       }
+       
+       /**
+        * @exports     WoltLab/WCF/Ui/Dropdown/Reusable
+        */
+       return {
+               /**
+                * Initializes a new reusable dropdown.
+                * 
+                * @param       {string}        identifier      internal identifier
+                * @param       {Element}       menu            dropdown menu element
+                */
+               init: function(identifier, menu) {
+                       if (_dropdowns.has(identifier)) {
+                               return;
+                       }
+                       
+                       var ghostElement = elCreate('div');
+                       ghostElement.id = 'reusableDropdownGhost' + _ghostElementId++;
+                       
+                       UiSimpleDropdown.initFragment(ghostElement, menu);
+                       
+                       _dropdowns.set(identifier, ghostElement.id);
+               },
+               
+               /**
+                * Returns the dropdown menu element.
+                * 
+                * @param       {string}        identifier      internal identifier
+                * @returns     {Element}       dropdown menu element
+                */
+               getDropdownMenu: function(identifier) {
+                       return UiSimpleDropdown.getDropdownMenu(_getDropdownName(identifier));
+               },
+               
+               /**
+                * Registers a callback invoked upon open and close.
+                * 
+                * @param       {string}        identifier      internal identifier
+                * @param       {function}      callback        callback function
+                */
+               registerCallback: function(identifier, callback) {
+                       UiSimpleDropdown.registerCallback(_getDropdownName(identifier), callback);
+               },
+               
+               /**
+                * Toggles a dropdown.
+                * 
+                * @param       {string}        identifier              internal identifier
+                * @param       {Element}       referenceElement        reference element used for alignment
+                */
+               toggleDropdown: function(identifier, referenceElement) {
+                       UiSimpleDropdown.toggleDropdown(_getDropdownName(identifier), referenceElement);
+               }
+       };
+});
index 13f9821594bdf406c89c23d80d9d49526c86b9ae..04a835c4d228b8c988630c121a0a1456886660c4 100644 (file)
@@ -22,7 +22,7 @@ define(
        /**
         * @exports     WoltLab/WCF/Ui/Dropdown/Simple
         */
-       var SimpleDropdown = {
+       return {
                /**
                 * Performs initial setup such as setting up dropdowns and binding listeners.
                 */
@@ -65,7 +65,7 @@ define(
                init: function(button, isLazyInitialization) {
                        this.setup();
                        
-                       if (button.classList.contains('jsDropdownEnabled') || elAttr(button, 'data-target')) {
+                       if (button.classList.contains('jsDropdownEnabled') || elData(button, 'target')) {
                                return false;
                        }
                        
@@ -91,11 +91,11 @@ define(
                                _menus.set(containerId, menu);
                                
                                if (!containerId.match(/^wcf\d+$/)) {
-                                       elAttr(menu, 'data-source', containerId);
+                                       elData(menu, 'source', containerId);
                                }
                        }
                        
-                       elAttr(button, 'data-target', containerId);
+                       elData(button, 'target', containerId);
                        
                        if (isLazyInitialization) {
                                setTimeout(function() { Core.triggerEvent(button, 'click'); }, 10);
@@ -111,11 +111,11 @@ define(
                initFragment: function(dropdown, menu) {
                        this.setup();
                        
-                       if (_dropdowns.has(dropdown)) {
+                       var containerId = DomUtil.identify(dropdown);
+                       if (_dropdowns.has(containerId)) {
                                return;
                        }
                        
-                       var containerId = DomUtil.identify(dropdown);
                        _dropdowns.set(containerId, dropdown);
                        _menuContainer.appendChild(menu);
                        
@@ -153,29 +153,30 @@ define(
                /**
                 * Toggles the requested dropdown between opened and closed.
                 * 
-                * @param       {string}        containerId     dropdown wrapper id
+                * @param       {string}        containerId             dropdown wrapper id
+                * @param       {Element=}      referenceElement        alternative reference element, used for reusable dropdown menus
                 */
-               toggleDropdown: function(containerId) {
-                       this._toggle(null, containerId);
+               toggleDropdown: function(containerId, referenceElement) {
+                       this._toggle(null, containerId, referenceElement);
                },
                
                /**
                 * Calculates and sets the alignment of given dropdown.
                 * 
-                * @param       {Element}       dropdown        dropdown wrapper element
-                * @param       {Element}       dropdownMenu    menu list element
+                * @param       {Element}       dropdown                dropdown wrapper element
+                * @param       {Element}       dropdownMenu            menu list element
+                * @param       {Element=}      alternateElement        alternative reference element for alignment
                 */
-               setAlignment: function(dropdown, dropdownMenu) {
+               setAlignment: function(dropdown, dropdownMenu, alternateElement) {
                        // check if button belongs to an i18n textarea
-                       var button = elBySel('.dropdownToggle', dropdown);
-                       var refDimensionsElement = null;
+                       var button = elBySel('.dropdownToggle', dropdown), refDimensionsElement;
                        if (button !== null && button.classList.contains('dropdownCaptionTextarea')) {
                                refDimensionsElement = button;
                        }
                        
-                       UiAlignment.set(dropdownMenu, dropdown, {
+                       UiAlignment.set(dropdownMenu, alternateElement || dropdown, {
                                pointerClassNames: ['dropdownArrowBottom', 'dropdownArrowRight'],
-                               refDimensionsElement: refDimensionsElement
+                               refDimensionsElement: refDimensionsElement || null
                        });
                },
                
@@ -203,11 +204,7 @@ define(
                 */
                isOpen: function(containerId) {
                        var menu = _menus.get(containerId);
-                       if (menu !== undefined && menu.classList.contains('dropdownOpen')) {
-                               return true;
-                       }
-                       
-                       return false;
+                       return (menu !== undefined && menu.classList.contains('dropdownOpen'));
                },
                
                /**
@@ -314,7 +311,7 @@ define(
                 */
                _onScroll: function() {
                        _dropdowns.forEach((function(dropdown, containerId) {
-                               if (elAttr(dropdown, 'data-is-overlay-dropdown-button') === true && dropdown.classList.contains('dropdownOpen')) {
+                               if (elData(dropdown, 'is-overlay-dropdown-button') === true && dropdown.classList.contains('dropdownOpen')) {
                                        this.setAlignment(dropdown, _menus.get(containerId));
                                }
                        }).bind(this));
@@ -335,24 +332,25 @@ define(
                /**
                 * Toggles the dropdown's state between open and close.
                 * 
-                * @param       {?Event}        event           event object, should be 'null' if targetId is given
-                * @param       {string=}       targetId        dropdown wrapper id
+                * @param       {?Event}        event                   event object, should be 'null' if targetId is given
+                * @param       {string=}       targetId                dropdown wrapper id
+                * @param       {Element=}      alternateElement        alternative reference element for alignment
                 * @return      {boolean}       'false' if event is not null
                 */
-               _toggle: function(event, targetId) {
+               _toggle: function(event, targetId, alternateElement) {
                        if (event !== null) {
                                event.preventDefault();
                                event.stopPropagation();
                                
-                               targetId = elAttr(event.currentTarget, 'data-target');
+                               targetId = elData(event.currentTarget, 'target');
                        }
                        
                        // check if 'isOverlayDropdownButton' is set which indicates if
                        // the dropdown toggle is in an overlay
                        var dropdown = _dropdowns.get(targetId);
-                       if (dropdown !== undefined && elAttr(dropdown, 'data-is-overlay-dropdown-button') === null) {
+                       if (dropdown !== undefined && elData(dropdown, 'is-overlay-dropdown-button') === null) {
                                var dialogContent = DomTraverse.parentByClass(dropdown, 'dialogContent');
-                               elAttr(dropdown, 'data-is-overlay-dropdown-button', (dialogContent !== null));
+                               elData(dropdown, 'is-overlay-dropdown-button', (dialogContent !== null));
                                
                                if (dialogContent !== null) {
                                        dialogContent.addEventListener('scroll', this._onDialogScroll.bind(this));
@@ -375,7 +373,7 @@ define(
                                        
                                        this._notifyCallbacks(containerId, 'open');
                                        
-                                       this.setAlignment(dropdown, menu);
+                                       this.setAlignment(dropdown, menu, alternateElement);
                                }
                        }).bind(this));
                        
@@ -385,6 +383,4 @@ define(
                        return (event === null);
                }
        };
-       
-       return SimpleDropdown;
 });
index 1be3aad9c0342271910fd491a1a02e27e620c672..e8fb712ee49397398cfb1f360342cf9540895fa4 100644 (file)
@@ -32,6 +32,7 @@ requirejs.config({
                        'Ui/Confirmation': 'WoltLab/WCF/Ui/Confirmation',
                        'Ui/Dialog': 'WoltLab/WCF/Ui/Dialog',
                        'Ui/Notification': 'WoltLab/WCF/Ui/Notification',
+                       'Ui/ReusableDropdown': 'WoltLab/WCF/Ui/Dropdown/Reusable',
                        'Ui/SimpleDropdown': 'WoltLab/WCF/Ui/Dropdown/Simple',
                        'Ui/TabMenu': 'WoltLab/WCF/Ui/TabMenu'
                }