just-add-juice
Version:
A responsive frontend framework with modular components.
468 lines (406 loc) • 17.9 kB
JavaScript
/* ========================================================================
JUICE -> COMPONENTS -> DROPDOWN
======================================================================== */
;(function (root, factory) {
// Set the plugin name
const plugin_name = 'Dropdown';
// Check if instantiation should be via amd, commonjs or the browser
if (typeof define === 'function' && define.amd) {
define([], factory(plugin_name));
} else if (typeof exports === 'object') {
module.exports = factory(plugin_name);
} else {
root[plugin_name] = factory(plugin_name);
}
}((window || module || {}), function(plugin_name) {
// Use strict mode
'use strict';
// Create an empty plugin object
const plugin = {};
// Set the plugin defaults
const defaults = {
animation: true,
animationClass: 'has-animation',
animationIn: 'fade-in-up',
animationOut: 'fade-out-down',
callbackInitializeBefore: () => {
console.log('Dropdown: callbackInitializeBefore');
},
callbackInitializeAfter: () => {
console.log('Dropdown: callbackInitializeAfter');
},
callbackShowBefore: () => {
console.log('Dropdown: callbackShowBefore');
},
callbackShowAfter: () => {
console.log('Dropdown: callbackShowAfter');
},
callbackHideBefore: () => {
console.log('Dropdown: callbackHideBefore');
},
callbackHideAfter: () => {
console.log('Dropdown: callbackHideAfter');
},
callbackRefreshBefore: () => {
console.log('Dropdown: callbackRefreshBefore');
},
callbackRefreshAfter: () => {
console.log('Dropdown: callbackRefreshAfter');
},
callbackDestroyBefore: () => {
console.log('Dropdown: callbackDestroyBefore');
},
callbackDestroyAfter: () => {
console.log('Dropdown: callbackDestroyAfter');
}
};
/**
* Constructor.
* @param {element} element The initialized element.
* @param {object} options The plugin options.
* @return {void}
*/
function Plugin(element, options) {
// Set the plugin object
plugin.this = this;
plugin.name = plugin_name;
plugin.element = element;
plugin.defaults = defaults;
plugin.options = options;
plugin.settings = Object.assign({}, defaults, options);
// Initialize the plugin
plugin.this.initialize();
}
/**
* Event handler to toggle a dropdown when the dropdown trigger is clicked.
* @param {object} event The event object.
* @return {void}
*/
const clickTriggerEventHandler = (event) => {
// Prevent the default action
event.preventDefault();
// Set the trigger
const $trigger = event.target;
// Set the container and dropdown
const $container = $trigger.closest('.has-dropdown');
const $dropdown = $container.querySelector('.dropdown');
// Check if the container is active and show/hide the dropdown
(!$container.classList.contains('is-active')
? plugin.this.show($dropdown)
: plugin.this.hide($dropdown)
);
};
/**
* Event handler to hide a dropdown when the dropdown container or any
* of the dropdown containers descendants lose focus.
* @param {object} event The event object.
* @return {void}
*/
const focusoutContainerEventHandler = (event) => {
// Set the current target, related target and dropdown
const $current = event.currentTarget;
const $related = event.relatedTarget;
const $dropdown = $current.querySelector('.dropdown');
// Check if a related target exists
if ($related) {
// Check if the related target is not a descendant of the current target
if (!$current.contains($related)) {
// Hide the dropdown
plugin.this.hide($dropdown);
}
} else {
// Hide the dropdown
plugin.this.hide($dropdown);
}
};
/**
* Event handler to show a dropdown when the mouse enters the dropdown container.
* @param {object} event The event object.
* @return {void}
*/
const mouseenterContainerEventHandler = (event) => {
// Set the container and dropdown
const $container = event.currentTarget;
const $dropdown = event.currentTarget.querySelector('.dropdown');
// Check if the container isn't active
if (!$container.classList.contains('is-active')) {
// Show the dropdown
plugin.this.show($dropdown);
}
};
/**
* Event handler to hide a dropdown when the mouse leaves the dropdown container.
* @param {object} event The event object.
* @return {void}
*/
const mouseleaveContainerEventHandler = (event) => {
// Set the container and dropdown
const $container = event.currentTarget;
const $dropdown = event.currentTarget.querySelector('.dropdown');
// Check if the container is active
if ($container.classList.contains('is-active')) {
// Hide the dropdown
plugin.this.hide($dropdown);
}
};
/**
* Public variables and methods.
* @type {object}
*/
Plugin.prototype = {
/**
* Initialize the plugin.
* @param {bool} silent Suppress callbacks.
* @return {void}
*/
initialize: (silent = false) => {
// Destroy the existing initialization silently
plugin.this.destroySilently();
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the initialize before callback
plugin.settings.callbackInitializeBefore.call();
}
// Set the containers
const $containers = document.querySelectorAll(plugin.element);
// Check if any containers exist
if ($containers) {
// Cycle through all of the containers
$containers.forEach(($container) => {
// Set the trigger and dropdown
const $trigger = $container.querySelector('.js-dropdown-trigger');
const $dropdown = $container.querySelector('.dropdown');
// Check if the trigger exists
if ($trigger) {
// Add a click event handler to the trigger to toggle the dropdown
$trigger.addEventListener('click', clickTriggerEventHandler);
}
// Add a focus out event handler to the container to hide the dropdown
$container.addEventListener('focusout', focusoutContainerEventHandler);
// Check if the dropdown is hoverable
if ($dropdown.classList.contains('is-hoverable') && document.documentElement.classList.contains('has-no-touch')) {
// Add mouse enter and mouse leave event handlers to the container to show and hide the dropdown
$container.addEventListener('mouseenter', mouseenterContainerEventHandler);
$container.addEventListener('mouseleave', mouseleaveContainerEventHandler);
}
});
}
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the initialize after callback
plugin.settings.callbackInitializeAfter.call();
}
},
/**
* Show a dropdown.
* @param {element} $dropdown The dropdown.
* @param {bool} silent Suppress callbacks.
* @return {void}
*/
show: ($dropdown, silent = false) => {
// Check if the dropdown exists and isn't animating in or out
if ($dropdown && !$dropdown.classList.contains('is-animating-in') && !$dropdown.classList.contains('is-animating-out')) {
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the show before callback
plugin.settings.callbackShowBefore.call();
}
// Set the container and content
const $container = $dropdown.closest('.has-dropdown');
const $content = $dropdown.querySelector('.dropdown__content');
// Add the active state hook to the container
$container.classList.add('is-active');
// Check if the dropdown is animated
if (plugin.settings.animation) {
// Set the animation in
const animation_in = $container.dataset.dropdownAnimationIn || plugin.settings.animationIn;
// Set the dropdown animation classes
$dropdown.classList.remove('has-animated');
$dropdown.classList.add('is-animating-in');
// Set the content animation classes
$content.classList.add(plugin.settings.animationClass);
$content.classList.add(animation_in);
// Add an animation end event listener to the content
$content.addEventListener('animationend', (event) => {
// Set the dropdown animation classes
$dropdown.classList.remove('is-animating-in');
$dropdown.classList.add('has-animated');
// Set the content animation classes
$content.classList.remove(plugin.settings.animationClass, animation_in);
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the show after callback
plugin.settings.callbackShowAfter.call();
}
}, {
once: true
});
} else {
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the show after callback
plugin.settings.callbackShowAfter.call();
}
}
}
},
/**
* Hide a dropdown.
* @param {element} $dropdown The dropdown.
* @param {bool} silent Suppress callbacks.
* @return {void}
*/
hide: ($dropdown, silent = false) => {
// Check if the dropdown exists and isn't animating in or out
if ($dropdown && !$dropdown.classList.contains('is-animating-in') && !$dropdown.classList.contains('is-animating-out')) {
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the hide before callback
plugin.settings.callbackHideBefore.call();
}
// Set the container and content
const $container = $dropdown.closest('.has-dropdown');
const $content = $dropdown.querySelector('.dropdown__content');
// Check if the dropdown is animated
if (plugin.settings.animation) {
// Set the animation out
const animation_out =
$container.dataset.dropdownAnimationOut ||
plugin.settings.animationOut;
// Set the dropdown animation classes
$dropdown.classList.remove('has-animated');
$dropdown.classList.add('is-animating-out');
// Set the content animation classes
$content.classList.add(plugin.settings.animationClass, animation_out);
// Add an animation end event listener to the content
$content.addEventListener('animationend', (event) => {
// Set the dropdown animation classes
$dropdown.classList.remove('is-animating-out');
$dropdown.classList.add('has-animated');
// Set the content animation classes
$content.classList.remove(plugin.settings.animationClass);
$content.classList.remove(animation_out);
// Remove the active state hook from the container
$container.classList.remove('is-active');
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the hide after callback
plugin.settings.callbackHideAfter.call();
}
}, {
once: true
});
} else {
// Remove the active state hook from the container
$container.classList.remove('is-active');
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the hide after callback
plugin.settings.callbackHideAfter.call();
}
}
}
},
/**
* Refresh the plugins initialization.
* @param {bool} silent Suppress callbacks.
* @return {void}
*/
refresh: (silent = false) => {
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the refresh before callback
plugin.settings.callbackRefreshBefore.call();
}
// Destroy the existing initialization
plugin.this.destroy(silent);
// Initialize the plugin
plugin.this.initialize(silent);
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the refresh after callback
plugin.settings.callbackRefreshAfter.call();
}
},
/**
* Destroy an existing initialization.
* @param {bool} silent Suppress callbacks.
* @return {void}
*/
destroy: (silent = false) => {
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the destroy before callback
plugin.settings.callbackDestroyBefore.call();
}
// Set the containers
const $containers = document.querySelectorAll(plugin.element);
// Check if any contains exists
if ($containers) {
// Cycle through all of the containers
$containers.forEach(($container) => {
// Set the trigger and dropdown
const $trigger = $container.querySelector('.js-dropdown-trigger');
const $dropdown = $container.querySelector('.dropdown');
// Check if the container is active
if ($container.classList.contains('is-active')) {
// Hide the dropdown
plugin.this.hide($dropdown, silent);
}
// Check if the trigger exists
if ($trigger) {
// Remove the click event handler from the trigger
$trigger.removeEventListener('click', clickTriggerEventHandler);
}
// Remove the focus out event handler from the container
$container.removeEventListener('focusout', focusoutContainerEventHandler);
// Check if the dropdown is hoverable
if ($dropdown.classList.contains('is-hoverable') && document.documentElement.classList.contains('has-no-touch')) {
// Remove the mouse enter and mouse leave event handlers from the container
$container.removeEventListener('mouseenter', mouseenterContainerEventHandler);
$container.addEventListener('mouseleave', mouseleaveContainerEventHandler);
}
});
}
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the destroy after callback
plugin.settings.callbackDestroyAfter.call();
}
},
/**
* Call the show method silently.
* @return {void}
*/
showSilently: ($dropdown) => {
// Call the show method silently
plugin.this.show($dropdown, true);
},
/**
* Call the hide method silently.
* @return {void}
*/
hideSilently: ($dropdown) => {
// Call the hide method silently
plugin.this.hide($dropdown, true);
},
/**
* Call the refresh method silently.
* @return {void}
*/
refreshSilently: () => {
// Call the refresh method silently
plugin.this.refresh(true);
},
/**
* Call the destroy method silently.
* @return {void}
*/
destroySilently: () => {
// Call the destroy method silently
plugin.this.destroy(true);
}
};
// Return the plugin
return Plugin;
}));