just-add-juice
Version:
A responsive frontend framework with modular components.
543 lines (469 loc) • 19.9 kB
JavaScript
/* ========================================================================
JUICE -> COMPONENTS -> ALERT
======================================================================== */
;(function (root, factory) {
// Set the plugin name
const plugin_name = 'Alert';
// 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 = {
center: false,
color: 'primary',
contentAnimation: true,
contentAnimationClass: 'has-animation',
contentAnimationIn: 'fade-in-up',
contentAnimationOut: 'fade-out-down',
continueButtonClass: null,
continueButtonText: 'Continue',
heading: '<h4>Heading</h4>',
overlayAnimation: true,
overlayAnimationClass: 'has-animation',
overlayAnimationIn: 'fade-in',
overlayAnimationOut: 'fade-out',
feedback: null,
size: null,
text: 'Lorem ipsum...',
callbackInitializeBefore: () => {
console.log('Alert: callbackInitializeBefore');
},
callbackInitializeAfter: () => {
console.log('Alert: callbackInitializeAfter');
},
callbackOpenBefore: () => {
console.log('Alert: callbackOpenBefore');
},
callbackOpenAfter: () => {
console.log('Alert: callbackOpenAfter');
},
callbackCloseBefore: () => {
console.log('Alert: callbackCloseBefore');
},
callbackCloseAfter: () => {
console.log('Alert: callbackCloseAfter');
},
callbackRefreshBefore: () => {
console.log('Alert: callbackRefreshBefore');
},
callbackRefreshAfter: () => {
console.log('Alert: callbackRefreshAfter');
},
callbackDestroyBefore: () => {
console.log('Alert: callbackDestroyBefore');
},
callbackDestroyAfter: () => {
console.log('Alert: callbackDestroyAfter')
},
callbackContinue: () => {
console.log('Alert: callbackContinue');
}
};
/**
* Constructor.
* @param {object} options The plugin options.
* @return {void}
*/
function Plugin(options) {
// Set the plugin object
plugin.this = this;
plugin.name = plugin_name;
plugin.defaults = defaults;
plugin.options = options;
plugin.settings = Object.assign({}, defaults, options);
// Initialize the plugin
plugin.this.initialize();
}
/**
* Build the alert.
* @return {void}
*/
const buildAlert = () => {
// Create the alert
const $alert = document.createElement('div');
const $content = document.createElement('div');
const $card = document.createElement('div');
const $head = document.createElement('div');
const $headings = document.createElement('div');
const $body = document.createElement('div');
const $foot = document.createElement('div');
const $buttons = document.createElement('div');
const $continue = document.createElement('button');
// Construct the alert
$alert.append($content);
$content.append($card);
// Construct the card
$card.append($head);
$card.append($body);
$card.append($foot);
// Construct the head
$head.append($headings);
$headings.insertAdjacentHTML('beforeend', plugin.settings.heading);
// Construct the body
$body.insertAdjacentHTML('beforeend', plugin.settings.text);
// Construct the foot
$foot.append($buttons);
// Construct the continue
$buttons.append($continue);
$continue.append(plugin.settings.continueButtonText);
// Add the alert classes
$alert.classList.add('overlay', 'alert');
$content.classList.add('alert__content');
$card.classList.add('alert__card');
$head.classList.add('alert__head');
$headings.classList.add('alert__headings');
$body.classList.add('alert__body');
$foot.classList.add('alert__foot');
$buttons.classList.add('alert__buttons');
$continue.classList.add(plugin.settings.continueButtonClass, 'js-alert-continue');
// Check if a center modifier exists
if (plugin.settings.center) {
// Add the center modifier class to the alert
$alert.classList.add('alert--center');
}
// Check if a size modifier exists
if (plugin.settings.size) {
// Add the size modifier class to the alert
$alert.classList.add(`is-${plugin.settings.size}`);
$continue.classList.add(`is-${plugin.settings.size}`);
}
// Check if a color modifier exists
if (plugin.settings.color) {
// Add the color modifier class to the alert
$alert.classList.add(`is-${plugin.settings.color}`);
$continue.classList.add(`is-${plugin.settings.color}`);
}
// Check if a feedback modifier exists
if (plugin.settings.feedback) {
// Add the feedback modifier class to the alert
$alert.classList.add(`has-${plugin.settings.feedback}`);
$continue.classList.add(`has-${plugin.settings.feedback}`);
}
// Append the alert to the document body
document.body.insertAdjacentHTML('beforeend', $alert.outerHTML);
};
/**
* Click event handler to continue an alert.
* @param {object} event The event object.
* @return {void}
*/
const clickAlertContinueEventHandler = (event) => {
// Check if the event target is the continue or a descendant of the continue
if (isTargetSelector(event.target, 'class', 'js-alert-continue')) {
// Prevent the default action
event.preventDefault();
// Call the continue callback
plugin.settings.callbackContinue.call();
}
};
/**
* Check if an event target is a target selector or a descendant of a target selector.
* @param {element} target The event target.
* @param {string} attribute The event target attribute to check.
* @param {string} selector The id/class selector.
* @return {bool} True if event target, false otherwise.
*/
const isTargetSelector = (target, attribute, selector) => {
// Check if the target is an element node
if (target.nodeType !== Node.ELEMENT_NODE) {
// Return false
return false;
}
// Start a switch statement for the attribute
switch (attribute) {
// Default
default:
// Return false
return false;
// Class
case 'class':
// Return true if event target, false otherwise
return ((target.classList.contains(selector)) || target.closest(`.${selector}`));
// Id
case ('id'):
// Return true if event target, false otherwise
return ((target.id == selector) || target.closest(`#${selector}`));
}
};
/**
* Trap focus to the modal.
* @param {element} $modal The modal.
* @return {void}
*/
const trapFocus = ($modal) => {
// Set the focusable elements
const $focusables = $modal.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [href]:not([disabled]), [tabindex]:not([tabindex="-1"])');
const $focusable_first = $focusables[0];
const $focusable_last = $focusables[$focusables.length - 1];
// Set the keycodes
const keycode_tab = 9;
// Add a keydown event listener to the modal to trap focus
$modal.addEventListener('keydown', function(event) {
// Start a switch event for the keycode
switch (event.keyCode) {
// Tab
case keycode_tab:
// Check if the shift key was pressed
if (event.shiftKey) {
// Check if the active element is the first focusable element
if (document.activeElement === $focusable_first) {
// Prevent the default action
event.preventDefault();
// Focus on the last focusable element
$focusable_last.focus();
}
} else {
if (document.activeElement === $focusable_last) {
// Prevent the default action
event.preventDefault();
// Focus on the first focusable element
$focusable_first.focus();
}
}
// Break the switch
break;
}
});
};
/**
* 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();
}
// Add a click event handler to continue an alert
document.addEventListener('click', clickAlertContinueEventHandler);
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the initialize after callback
plugin.settings.callbackInitializeAfter.call();
}
// Open the alert
plugin.this.open();
},
/**
* Open the alert.
* @param {bool} silent Suppress callbacks.
* @return {void}
*/
open: (silent = false) => {
// Check if an overlay isn't already open
if (!document.body.classList.contains('has-overlay') || !document.querySelector('.overlay')) {
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the open before callback
plugin.settings.callbackOpenBefore.call();
}
// Add the overlay state hook to the document body
document.body.classList.add('has-overlay');
// Build the alert
buildAlert();
// Set the alert elements
const $alert = document.querySelector('.alert');
const $content = $alert.querySelector('.alert__content');
const $continue = $alert.querySelector('.js-alert-continue');
// Set the plugin alert
plugin.this.alert = $alert;
// Set the alert tabindex and focus on the alert
$alert.setAttribute('tabindex', -1);
$alert.focus();
// Trap focus inside the alert
trapFocus($alert);
// Check if the overlay is animated
if (plugin.settings.overlayAnimation) {
// Set the alert animation classes
$alert.classList.add('is-animating-in', plugin.settings.overlayAnimationClass, plugin.settings.overlayAnimationIn);
// Add an animation end event listener to the alert
$alert.addEventListener('animationend', (event) => {
// Set the the alert animation classes
$alert.classList.remove('is-animating-in', plugin.settings.overlayAnimationClass, plugin.settings.overlayAnimationIn);
$alert.classList.add('has-animated');
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the open after callback
plugin.settings.callbackOpenAfter.call();
}
}, {
once: true
});
} else {
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the open after callback
plugin.settings.callbackOpenAfter.call();
}
}
// Check if the content is animated
if (plugin.settings.contentAnimation) {
// Set the content animation classes
$content.classList.add('is-animating-in', plugin.settings.contentAnimationClass, plugin.settings.contentAnimationIn);
// Add an animation end event listener to the content
$content.addEventListener('animationend', (event) => {
// Set the the content animation classes
$content.classList.remove('is-animating-in', plugin.settings.contentAnimationClass, plugin.settings.contentAnimationIn);
$content.classList.add('has-animated');
}, {
once: true
});
}
}
},
/**
* Close a alert.
* @param {bool} silent Suppress callbacks.
* @return {void}
*/
close: (silent = false) => {
// Set the alert
const $alert = plugin.this.alert;
// Check if the alert exists and an overlay alert is open
if ($alert && (document.body.classList.contains('has-overlay') || document.querySelector('.overlay.alert'))) {
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the close before callback
plugin.settings.callbackCloseBefore.call();
}
// Set the content
const $content = $alert.querySelector('.alert__content');
// Check if the overlay is animated
if (plugin.settings.overlayAnimation) {
// Set the alert animation classes
$alert.classList.add('is-animating-out', plugin.settings.overlayAnimationClass, plugin.settings.overlayAnimationOut);
// Add an animation end event listener to the alert
$alert.addEventListener('animationend', (event) => {
// Remove the alert
$alert.remove();
// Remove the overlay state hook from the document body
document.body.classList.remove('has-overlay');
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the close after callback
plugin.settings.callbackCloseAfter.call();
}
}, {
once: true
});
} else {
// Remove the alert
$alert.remove();
// Remove the overlay state hook from the document body
document.body.classList.remove('has-overlay');
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the close after callback
plugin.settings.callbackCloseAfter.call();
}
}
// Check if the content is animated
if (plugin.settings.contentAnimation) {
// Set the content animation classes
$content.classList.add('is-animating', plugin.settings.contentAnimationClass, plugin.settings.contentAnimationOut);
// Add an animation end event listener to the content
$content.addEventListener('animationend', (event) => {
// Set the the content animation classes
$content.classList.remove('is-animating', plugin.settings.contentAnimationClass, plugin.settings.contentAnimationOut);
$content.classList.add('has-animated');
}, {
once: true
});
}
}
},
/**
* 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();
}
// Remove the click event handler to continue an alert
document.removeEventListener('click', clickAlertContinueEventHandler);
// Check if the callbacks should not be suppressed
if (!silent) {
// Call the destroy after callback
plugin.settings.callbackDestroyAfter.call();
}
},
/**
* Call the open method silently.
* @return {void}
*/
openSilently: () => {
// Call the open method silently
plugin.this.open(true);
},
/**
* Call the close method silently.
* @return {void}
*/
closeSilently: () => {
// Call the close method silently
plugin.this.close(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;
}));