Source: admin/javascript/engine/widgets/button_dropdown.js

/* --------------------------------------------------------------
 button_dropdown.js 2016-02-23
 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.
 * 
 * **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).
 * 
 * ### 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>
 * ```
 *
 * @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'
			},
			
			/**
			 * 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'];
			
			// 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 (elementObject.config_key && configuration[elementObject.config_key]) {
					$element.attr(options.config_value_attribute, configuration[elementObject.config_key]);
				}
				
				// 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;
	});