From ed40929cae8149ad9de4a3d0466ad4b1810520e5 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Tue, 17 Nov 2015 00:51:17 +0100 Subject: [PATCH] Added reusable dropdown menus 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. --- .../js/WoltLab/WCF/Ui/Dropdown/Reusable.js | 82 +++++++++++++++++++ .../js/WoltLab/WCF/Ui/Dropdown/Simple.js | 58 ++++++------- wcfsetup/install/files/js/require.config.js | 1 + 3 files changed, 110 insertions(+), 31 deletions(-) create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Reusable.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 index 0000000000..b0acdc22cb --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Reusable.js @@ -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 + * @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); + } + }; +}); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Simple.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Simple.js index 13f9821594..04a835c4d2 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Simple.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Simple.js @@ -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; }); diff --git a/wcfsetup/install/files/js/require.config.js b/wcfsetup/install/files/js/require.config.js index 1be3aad9c0..e8fb712ee4 100644 --- a/wcfsetup/install/files/js/require.config.js +++ b/wcfsetup/install/files/js/require.config.js @@ -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' } -- 2.20.1