/* --------------------------------------------------------------
button_dropdown.js 2016-07-15
Gambio GmbH
http://www.gambio.de
Copyright (c) 2016 Gambio GmbH
Released under the GNU General Public License (Version 2)
[http://www.gnu.org/licenses/gpl-2.0.html]
--------------------------------------------------------------
*/
/**
* ## Button Dropdown Widget
*
* Adds the dropdown functionality to multiple elements inside a parent container. You can add new HTML
* options to each dropdown instance manually or dynamically through the Admin/Libs/button_dropdown library.
*
* Optionally, the widget has also the ability to store the last clicked option and display it as the default
* action the next time the page is loaded. This is very useful whenever there are many options inside the
* dropdown list.
*
* ### Parent Container Options
*
* **Configuration Keys | `data-button_dropdown-config_keys` | String | Optional**
*
* Provide a unique key which will be used to store the latest user selection. Prefer to prefix your config key
* in order to avoid collisions with other instances of the widget.
*
* **User ID | `data-button_dropdown-user_id` | Number | Optional**
*
* Give the current user database ID that will be used to associate his latest selection with the corresponding
* button dropdown widget.
*
* ### Widget Instance Options
*
* **Use Button Dropdown | `data-use-button_dropdown` | Boolean | Required**
*
* This option-flag will mark the elements inside the parent container, that will be converted into
* button-dropdown widgets.
*
* **Configuration Key | `data-config_key` | String | Required**
*
* Provide the configuration key for the single button-dropdown instance.
*
* **Configuration Value | `data-config_key` | String | Optional**
*
* Provide directly the configuration value in order to avoid extra AJAX requests.
*
* **Custom Caret Button Class | `data-custom_caret_btn_class` | String | Optional**
*
* Attach additional classes to the caret button element (the one with the arrow). Use this option if you
* want to add a class that the primary button already has so that both share the same style (e.g. btn-primary).
*
* Change buttons disable attribute value by adding option `data-button_dropdown-disabled_state` on parent element
*
* ### Example
* ```html
* <!-- This element represents the parent container. -->
* <div
* data-gx-widget="button_dropdown"
* data-button_dropdown-config_keys="order-single order-multi"
* data-button_dropdown-user_id="2">
*
* <!-- This element represents the button dropdown widget. -->
* <div
* data-use-button_dropdown="true"
* data-config_key="order-single"
* data-custom_caret_btn_class="class1">
* <button>Primary Button</button>
* <ul>
* <li><span>Change status</span></li>
* <li><span>Delete</span></li>
* </ul>
* </div>
* </div>
* ```
*
* **Notice:** This widget was built for usage in compatibility mode. The new admin pages use the Bootstrap
* button dropdown markup which already functions like this module. Do not use it on new admin pages.
*
* @module Admin/Widgets/button_dropdown
*/
gx.widgets.module(
'button_dropdown',
['user_configuration_service'],
function (data) {
'use strict';
// ------------------------------------------------------------------------
// VARIABLE DEFINITION
// ------------------------------------------------------------------------
var
/**
* Widget Reference
* @type {object}
*/
$this = $(this),
/**
* UserConfigurationService alias.
* @type {object}
*/
userConfigurationService = jse.libs.user_configuration_service,
/**
* Caret button template.
* @type {string}
*/
caretButtonTemplate = '<button class="btn" type="button"><i class="fa fa-caret-down"></i></button>',
/**
* Default Widget Options
* @type {object}
*/
defaults = {
/**
* Fade animation options.
* @type {object}
*/
fade: {
duration: 300,
easing: 'swing'
},
/**
* String for dropdown selector.
* This selector is used to find and activate all button dropdowns.
*
* @type {string}
*/
dropdown_selector: '[data-use-button_dropdown]',
/**
* Attribute which represents the user configuration value.
* The value of this attribute will be set.
*
* @type {string}
*/
config_value_attribute: 'data-configuration_value',
/**
* Used to disable buttons if needed
*
* @type {bool}
*/
disabled_state: false
},
/**
* Final Widget Options
* @type {object}
*/
options = $.extend(true, {}, defaults, data),
/**
* Element selector shortcuts.
* @type {object}
*/
selectors = {
element: options.dropdown_selector,
mainButton: 'button:nth-child(1)',
caretButton: 'button:nth-child(2)'
},
/**
* Module Object
* @type {object}
*/
module = {};
/**
* Split space-separated entries to array values.
* @type {array}
*/
options.config_keys = options.config_keys ? options.config_keys.split(' ') : [];
// ------------------------------------------------------------------------
// PRIVATE METHODS - INITIALIZATION
// ------------------------------------------------------------------------
/**
* Loads the user configuration values for each provided key.
* Returns a Deferred object with an object with configuration
* as key and respective values or null if no request conditions are set.
*
* @returns {jQuery.Deferred}
* @private
*/
var _loadConfigurations = function () {
/**
* Main deferred object which will be returned.
* @type {jQuery.Deferred}
*/
var deferred = $.Deferred();
/**
* This array will contain all deferred ajax request to the user configuration service.
* @example
* [Deferred, Deferred]
* @type {array}
*/
var configDeferreds = [];
/**
* User configuration key and values storage.
* @example
* {
* configKey: 'configValue'
* }
* @type {object}
*/
var configValues = {};
// Return immediately if the user configuration service is not needed.
if (!options.user_id || !options.config_keys.length) {
return deferred.resolve(null);
}
// Iterate over each configuration value provided in the element
$.each(options.config_keys, function (index, configKey) {
// Create deferred object for configuration value fetch.
var configDeferred = $.Deferred();
// Fetch configuration value from service.
// Adds the fetched value to the `configValues` object and resolves the promise.
userConfigurationService.get({
data: {
userId: options.user_id,
configurationKey: configKey
},
onSuccess: function (response) {
configValues[configKey] = response.configurationValue;
configDeferred.resolve();
},
onError: function () {
configDeferred.resolve();
}
});
configDeferreds.push(configDeferred);
});
// If all requests for the configuration values has been processed
// then the main promise will be resolved with all configuration values as given parameter.
$.when.apply(null, configDeferreds).done(function () {
deferred.resolve(configValues);
});
// Return deferred object.
return deferred;
};
/**
* Finds all dropdown elements.
* Returns a deferred object with an element list object.
* This function hides the dropdown elements.
*
* @return {jQuery.Deferred}
* @private
*/
var _findDropdownElements = function () {
/**
* Deferred object which will be returned.
* @type {jQuery.Deferred}
*/
var deferred = $.Deferred();
/**
* Elements with element and data attribute informations.
* @example
* [{
* element: <div>,
* custom_caret_btn_class: 'btn-primary'
* configKey: 'orderMultiSelect'
* }]
* @type {array}
*/
var elements = [];
/**
* Array of data attributes for the dropdown elements which will be checked.
* @type {array}
*/
var dataAttributes = ['custom_caret_btn_class', 'config_key', 'config_value'];
// Find dropdown elements when DOM is ready
// and resolve promise passing found elements as parameter.
$(document).ready(function () {
$this.find(options.dropdown_selector).each(function (index, element) {
/**
* jQuery wrapped element shortcut.
* @type {jQuery}
*/
var $element = $(element);
/**
* Element info object.
* Will be pushed to `elements` array.
* @example
* {
* element: <div>,
* custom_caret_btn_class: 'btn-primary'
* configKey: 'orderMultiSelect'
* }
* @type {object}
*/
var elementObject = {};
// Add element to element info object.
elementObject.element = element;
// Iterate over each data attribute key and check for data attribute existence.
// If data-attribute exists, the key and value will be added to element info object.
$.each(dataAttributes, function (index, attribute) {
if (attribute in $element.data()) {
elementObject[attribute] = $element.data(attribute);
}
});
// Push this element info object to `elements` array.
elements.push(elementObject);
// Hide element
$element.hide();
});
// Resolve the promise passing in the elements as argument.
deferred.resolve(elements);
});
// Return deferred object.
return deferred;
};
// ------------------------------------------------------------------------
// PRIVATE METHODS - DROPDOWN TOGGLE
// ------------------------------------------------------------------------
/**
* Shows dropdown action list.
*
* @param {HTMLElement} element Dropdown action list element.
* @private
*/
var _showDropdown = function (element) {
// Perform fade in.
$(element)
.stop()
.addClass('hover')
.fadeIn(options.fade);
// Fix position.
_repositionDropdown(element);
};
/**
* Hides dropdown action list.
*
* @param {HTMLElement} element Dropdown action list element.
* @private
*/
var _hideDropdown = function (element) {
// Perform fade out.
$(element)
.stop()
.removeClass('hover')
.fadeOut(options.fade);
};
/**
* Fixes the dropdown action list to ensure that the action list is always visible.
*
* Sometimes when the button dropdown widget is near the window borders the list might
* not be visible. This function will change its position in order to always be visible.
*
* @param {HTMLElement} element Dropdown action list element.
* @private
*/
var _repositionDropdown = function (element) {
// Wrap element in jQuery and save shortcut to dropdown action list element.
var $list = $(element);
// Reference to button element.
var $button = $list.closest(options.dropdown_selector);
// Reset any possible CSS position modifications.
$list.css({left: '', top: ''});
// Check dropdown position and perform reposition if needed.
if ($list.offset().left + $list.width() > window.innerWidth) {
var toMoveLeftPixels = $list.width() - $button.width();
$list.css('margin-left', '-' + (toMoveLeftPixels) + 'px');
}
if ($list.offset().top + $list.height() > window.innerHeight) {
var toMoveUpPixels = $list.height() + 10; // 10px fine-tuning
$list.css('margin-top', '-' + (toMoveUpPixels) + 'px');
}
};
// ------------------------------------------------------------------------
// PRIVATE METHODS - EVENT HANDLERS
// ------------------------------------------------------------------------
/**
* Handles click events on the main button (action button).
* Performs main button action.
*
* @param {jQuery.Event} event
* @private
*/
var _mainButtonClickHandler = function (event) {
event.preventDefault();
event.stopPropagation();
$(this).trigger('perform:action');
};
/**
* Handles click events on the dropdown button (caret button).
* Shows or hides the dropdown action list.
*
* @param {jQuery.Event} event
* @private
*/
var _caretButtonClickHandler = function (event) {
event.preventDefault();
event.stopPropagation();
/**
* Shortcut reference to dropdown action list element.
* @type {jQuery}
*/
var $list = $(this).siblings('ul');
/**
* Determines whether the dropdown action list is visible.
* @type {boolean}
*/
var listIsVisible = $list.hasClass('hover');
// Hide or show dropdown, dependent on its visibility state.
if (listIsVisible) {
_hideDropdown($list);
} else {
_showDropdown($list);
}
};
/**
* Handles click events on the dropdown action list.
* Hides the dropdown, saves the chosen value through
* the user configuration service and perform the selected action.
*
* @param {jQuery.Event} event
* @private
*/
var _listItemClickHandler = function (event) {
event.preventDefault();
event.stopPropagation();
/**
* Reference to `this` element, wrapped in jQuery.
* @type {jQuery}
*/
var $self = $(this);
/**
* Reference to dropdown action list element.
* @type {jQuery}
*/
var $list = $self.closest('ul');
/**
* Reference to button dropdown element.
* @type {jQuery}
*/
var $button = $self.closest(options.dropdown_selector);
// Hide dropdown.
_hideDropdown($list);
// Save user configuration data.
var configKey = $button.data('config_key'),
configValue = $self.data('value');
if (configKey && configValue) {
_saveUserConfiguration(configKey, configValue);
}
// Perform action.
$self.trigger('perform:action');
};
/**
* Handles click events outside of the button area.
* Hides multiple opened dropdowns.
* @param {jQuery.Event} event
* @private
*/
var _outsideClickHandler = function (event) {
/**
* Element shortcut to all opened dropdown action lists.
* @type {jQuery}
*/
var $list = $('ul.hover');
// Hide all opened dropdowns.
_hideDropdown($list);
};
// ------------------------------------------------------------------------
// PRIVATE METHODS - CREATE WIDGETS
// ------------------------------------------------------------------------
/**
* Adds the dropdown functionality to the buttons.
*
* Developers can manually add new `<li>` items to the list in order to display more options
* to the users.
*
* This function fades the dropdown elements in.
*
* @param {array} elements List of elements infos object which contains the element itself and data attributes.
* @param {object} configuration Object with fetched configuration key and values.
*
* @return {jQuery.Deferred}
* @private
*/
var _makeWidgets = function (elements, configuration) {
/**
* Deferred object which will be returned.
* @type {jQuery.Deferred}
*/
var deferred = $.Deferred();
/**
* The secondary button which will toggle the list visibility.
* @type {jQuery}
*/
var $secondaryButton = $(caretButtonTemplate);
// Iterate over each element and create dropdown widget.
$.each(elements, function (index, elementObject) {
/**
* Button dropdown element.
* @type {jQuery}
*/
var $element = $(elementObject.element);
/**
* Button dropdown element's buttons.
* @type {jQuery}
*/
var $button = $element.find('button:first');
/**
* Cloned caret button template.
* @type {jQuery}
*/
var $caretButton = $secondaryButton.clone();
// Add custom class to template, if defined.
if (elementObject.custom_caret_btn_class) {
$caretButton.addClass(elementObject.custom_caret_btn_class);
}
// Add CSS class to button and place the caret button.
$button
.addClass('btn')
.after($caretButton);
// Add class to dropdown button element.
$element
.addClass('js-button-dropdown');
// Add configuration value to container, if key and value exist.
if (configuration && elementObject.config_key && configuration[elementObject.config_key] || elementObject.config_value) {
var value = elementObject.config_value || configuration[elementObject.config_key];
$element.attr(options.config_value_attribute, value);
}
// add disabled state if exists
$button.prop('disabled', options.disabled_state);
$caretButton.prop('disabled', options.disabled_state);
// Attach event handler: Click on first button (main action button).
$element.on('click', selectors.mainButton, _mainButtonClickHandler);
// Attach event handler: Click on dropdown button (caret button).
$element.on('click', selectors.caretButton, _caretButtonClickHandler);
// Attach event handler: Click on dropdown action list item.
$element.on('click', 'ul span, ul a', _listItemClickHandler);
// Fade in element.
$element.fadeIn(options.fade.duration, function () {
$element.css('display', '');
});
});
// Attach event handler: Close dropdown on outside click.
$(document).on('click', _outsideClickHandler);
// Resolve promise.
deferred.resolve();
// Return deferred object.
return deferred;
};
// ------------------------------------------------------------------------
// PRIVATE METHODS - SAVE USER CONFIGURATION
// ------------------------------------------------------------------------
/**
* Saves a user configuration value.
*
* @param {string} key Configuration key.
* @param {string} value Configuration value.
* @private
*/
var _saveUserConfiguration = function (key, value) {
// Throw error if no complete data has been provided.
if (!key || !value) {
throw new Error('No configuration data passed');
}
// Save value to database via user configuration service.
userConfigurationService.set({
data: {
userId: options.user_id,
configurationKey: key,
configurationValue: value
}
});
};
// ------------------------------------------------------------------------
// INITIALIZATION
// ------------------------------------------------------------------------
/**
* Initialize method of the module, called by the engine.
*/
module.init = function (done) {
$.when(_findDropdownElements(), _loadConfigurations())
.then(_makeWidgets)
.then(done);
};
// Return data to module engine.
return module;
});