Source: admin/javascript/engine/extensions/datatable_checkbox_mapping.js

/* --------------------------------------------------------------
 datatable_checkbox_mapping.js 2016-10-18
 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]
 --------------------------------------------------------------
 */

/**
 * ## DataTable Checkbox Mapping Extension
 *
 * This extension maps the bulk actions from a datatable dropdown to the selected checkbox carets. Bind this
 * extension into a datatable element which has a first.
 *
 * ### Options
 *
 * **Bulk Action Selector | `data-datatable_checkbox_mapping-bulk-action` | String | Optional**
 *
 * Provide a selector for the bulk action dropdown widget. Default value is '.bulk-action'.
 *
 * **Bulk Selection Checkbox Selector | `data-datatable_checkbox_mapping-bulk-selection` | String | Optional**
 *
 * Provide a selector for the bulk selection checkbox in the table header. Default value is '.bulk-selection'.
 * 
 * **Row Selection Checkbox Selector | `data-datatable_checkbox_mapping-row-selection` | String | Optional**
 *
 * Provide a selector for the row selection checkbox in the table body. Default value is 'tbody tr input:checkbox'.
 *
 * **Caret Icon Class | `data-datatable_checkbox_mapping-caret-icon-class` | String | Optional**
 *
 * Provide a FontAwesome icon class for the checkbox caret. Default value is 'fa-caret-down'. Provide only the class
 * name without dots or the "fa" class.
 *
 * @module Admin/Extensions/datatable_checkbox_mapping
 */
gx.extensions.module(
	'datatable_checkbox_mapping',
	
	[],
	
	function(data) {
		
		'use strict';
		
		// ------------------------------------------------------------------------
		// VARIABLES
		// ------------------------------------------------------------------------
		
		/**
		 * Module Selector
		 *
		 * @type {jQuery}
		 */
		const $this = $(this);
		
		/**
		 * Default Options
		 *
		 * @type {Object}
		 */
		const defaults = {
			bulkAction: '.bulk-action',
			bulkSelection: '.bulk-selection',
			caretIconClass: 'fa-caret-down',
			rowSelection: 'tbody tr input:checkbox'
		};
		
		/**
		 * Final Options
		 *
		 * @type {Object}
		 */
		const options = $.extend(true, {}, defaults, data);
		
		/**
		 * Bulk Action Selector
		 *
		 * @type {jQuery}
		 */
		const $bulkAction = $(options.bulkAction);
		
		/**
		 * Bulk Selection Selector
		 *
		 * @type {jQuery}
		 */
		const $bulkSelection = $this.find(options.bulkSelection).last();
		
		/**
		 * Module Instance
		 *
		 * @type {Object}
		 */
		const module = {};
		
		// ------------------------------------------------------------------------
		// FUNCTIONS
		// ------------------------------------------------------------------------
		
		/**
		 * Toggle the dropdown menu under the caret.
		 *
		 * @param {jQuery.Event} event Triggered event.
		 */
		function _toggleDropdownMenu(event) {
			event.stopPropagation();
			event.preventDefault();
			
			if ($bulkAction.hasClass('open')) {
				$bulkAction.removeClass('open');
				return;
			}
			
			const caretPosition = $(event.target).offset();
			const $dropdownMenu = $bulkAction.find('.dropdown-menu');
			
			// Open the dropdown menu.
			$bulkAction.addClass('open');
			
			// Reposition the dropdown menu near the clicked caret.
			$dropdownMenu.offset({
				top: caretPosition.top + 16,
				left: caretPosition.left
			});
			
			// Don't show the long empty dropdown menu box when it is repositioned.
			$dropdownMenu.css({bottom: 'initial'});
			
			// Show the dropdown menu under or above the caret, depending on the viewport.
			if (_dropdownIsOutOfView($dropdownMenu)) {
				$dropdownMenu.offset({
					top: caretPosition.top - $dropdownMenu.outerHeight(),
					left: caretPosition.left
				});
			}
		}
		
		/**
		 * Reset the dropdown position to its original state.
		 */
		function _resetDropdownPosition() {
			$bulkAction.find('.dropdown-menu').css({
				top: '',
				left: '',
				bottom: ''
			});
		}
		
		/**
		 * Add a caret to the table head checked checkbox.
		 */
		function _addCaretToBulkSelection() {
			const $th = $bulkSelection.parents('th');
			
			if ($th.find('.' + options.caretIconClass).length === 0) {
				$th.append(`<i class="fa ${options.caretIconClass}"></i>`);
			}
		}
		
		/**
		 * Remove the caret from the bulk selection checkbox.
		 */
		function _removeCaretFromBulkSelection() {
			$bulkSelection.parents('th').find('.' + options.caretIconClass).remove();
		}
		
		/**
		 * Add a caret to the checked checkbox.
		 *
		 * @param {jQuery.Event} event Triggered event.
		 */
		function _addCaretToActivatedCheckbox(event) {
			$(event.target).parents('td').append(`<i class="fa ${options.caretIconClass}"></i>`);
		}
		
		/**
		 * Remove the caret from the checkbox if the checkbox is unchecked.
		 *
		 * @param {jQuery.Event} event Triggered event.
		 */
		function _removeCaretFromCheckbox(event) {
			$(event.target).parents('tr').find('.' + options.caretIconClass).remove();
		}
		
		/**
		 * Start listening for click events for the caret symbol.
		 *
		 * When the caret symbol gets clicked, show the dropdown menu.
		 */
		function _listenForCaretClickEvents() {
			$this.find('tr .' + options.caretIconClass).off('click').on('click', _toggleDropdownMenu);
		}
		
		/**
		 * Set the bulk selection state.
		 *
		 * @param {Boolean} isChecked Whether the checkbox will be checked or not.
		 */
		function _setBulkSelectionState(isChecked) {
			$bulkSelection.prop('checked', isChecked);
			
			if (isChecked) {
				$bulkSelection.parents('.single-checkbox').addClass('checked');
				_addCaretToBulkSelection();
				_listenForCaretClickEvents();
			} else {
				$bulkSelection.parents('.single-checkbox').removeClass('checked');
				_removeCaretFromBulkSelection();
			}
		}
		
		/**
		 * Checks if the provided dropdown is outside of the viewport (in height).
		 *
		 * @param {jQuery} $dropdownMenu Dropdown menu selector.
		 *
		 * @return {Boolean}
		 */
		function _dropdownIsOutOfView($dropdownMenu) {
			const dropDownMenuOffset = $dropdownMenu.offset().top + $dropdownMenu.outerHeight() + 50;
			const windowHeight = window.innerHeight + $(window).scrollTop();
			return dropDownMenuOffset > windowHeight;
		}
		
		/**
		 * On Single Checkbox Ready Event
		 *
		 * Bind the checkbox mapping functionality on the table. We need to wait for the "single_checkbox:ready",
		 * that will be triggered with every table re-draw. Whenever a row checkbox is clicked the bulk-action
		 * caret icon will be added next to it.
		 */
		function _onSingleCheckboxReady() {
			// Find all checkboxes table body checkboxes.
			const $tableBodyCheckboxes = $this.find(options.rowSelection);
			
			// Table data checkbox event handling.
			$tableBodyCheckboxes.on('change', event => {
				// Close any open dropdown menus.
				$bulkAction.removeClass('open');
				
				if ($(event.target).prop('checked')) {
					_addCaretToActivatedCheckbox(event);
					_listenForCaretClickEvents();
				} else {
					_removeCaretFromCheckbox(event);
				}
				
				// Activate the table head checkbox if all checkboxes are activated. Otherwise deactivate it.
				_setBulkSelectionState($tableBodyCheckboxes.not(':checked').length === 0);
			});
		}
		
		/**
		 * Add or remove the caret from the table head checkbox.
		 *
		 * @param {jQuery.Event} event
		 */
		function _onBulkSelectionChange(event) {
			if ($bulkSelection.parents('.single-checkbox').length === 0) {
				return; // Do not proceed with the function if the thead single-checkbox is not ready yet.
			}
			
			if ($bulkSelection.prop('checked')) {
				_addCaretToBulkSelection();
				_listenForCaretClickEvents();
			} else {
				_removeCaretFromBulkSelection(event);
			}
		}
		
		/**
		 *  Event handling for the original dropdown button click.
		 */
		function _onBulkActionDropdownToggleClick() {
			_resetDropdownPosition();
		}
		
		// ------------------------------------------------------------------------
		// INITIALIZATION
		// ------------------------------------------------------------------------
		
		module.init = function(done) {
			$this.on('single_checkbox:ready', _onSingleCheckboxReady);
			$bulkSelection.on('change', _onBulkSelectionChange);
			$bulkAction.find('.dropdown-toggle').on('click', _onBulkActionDropdownToggleClick);
			done();
		};
		return module;
	});