UNPKG

just-add-juice

Version:

A responsive frontend framework with modular components.

616 lines (527 loc) 22.9 kB
/* ======================================================================== JUICE -> COMPONENTS -> MODAL ======================================================================== */ ;(function (root, factory) { // Set the plugin name const plugin_name = 'Modal'; // 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 = { close: true, closeContent: '<button type="button" class="button--component is-huge js-modal-close"><i class="fas fa-times"></i></button>', closeEsc: true, contentAnimation: true, contentAnimationClass: 'has-animation', contentAnimationIn: 'fade-in-up', contentAnimationOut: 'fade-out-down', overlayAnimation: true, overlayAnimationClass: 'has-animation', overlayAnimationIn: 'fade-in', overlayAnimationOut: 'fade-out', callbackInitializeBefore: () => { console.log('Modal: callbackInitializeBefore'); }, callbackInitializeAfter: () => { console.log('Modal: callbackInitializeAfter'); }, callbackOpenBefore: () => { console.log('Modal: callbackOpenBefore'); }, callbackOpenAfter: () => { console.log('Modal: callbackOpenAfter'); }, callbackCloseBefore: () => { console.log('Modal: callbackCloseBefore'); }, callbackCloseAfter: () => { console.log('Modal: callbackCloseAfter'); }, callbackRefreshBefore: () => { console.log('Modal: callbackRefreshBefore'); }, callbackRefreshAfter: () => { console.log('Modal: callbackRefreshAfter'); }, callbackDestroyBefore: () => { console.log('Modal: callbackDestroyBefore'); }, callbackDestroyAfter: () => { console.log('Modal: callbackDestroyAfter') }, callbackCancel: () => { console.log('Modal: callbackCancel'); }, callbackContinue: () => { console.log('Modal: callbackContinue'); }, callbackEsc: () => { console.log('Modal: callbackEsc'); } }; /** * 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(); } /** * Build the modal. * @param {element} $target The modal target. * @return {void} */ const buildModal = ($target) => { // Create the modal const $modal = document.createElement('div'); // Add the modal classes $modal.classList.add('overlay', 'modal'); // Set the modal modifiers const center = $target.dataset.modalCenter || plugin.settings.center; const close = $target.dataset.modalClose || plugin.settings.close; // Check if a center modifier exists if (center) { // Add the center modifier class to the modal $modal.classList.add('modal--center'); } // Append the target to the modal $modal.insertAdjacentHTML('beforeend', $target.innerHTML); // Check if a close modifier exists if (close) { // Create the close const $close = document.createElement('span'); // Add the close classes $close.classList.add('modal__close'); // Set the close content $close.innerHTML = plugin.settings.closeContent; // Append the close to the modal $modal.append($close); } // Append the modal to the document body document.body.insertAdjacentHTML('beforeend', $modal.outerHTML); }; /** * Click event handler to cancel a modal. * @param {object} event The event object. * @return {void} */ const clickModalCancelEventHandler = (event) => { // Check if the event target is the cancel or a descendant of the cancel if (isTargetSelector(event.target, 'class', 'js-modal-cancel')) { // Prevent the default action event.preventDefault(); // Call the cancel callback plugin.settings.callbackCancel.call(); } }; /** * Click event handler to close a modal. * @param {object} event The event object. * @return {void} */ const clickModalCloseEventHandler = (event) => { // Check if the event target is the close or a descendant of the close if (isTargetSelector(event.target, 'class', 'js-modal-close')) { // Prevent the default action event.preventDefault(); // Set the close and modal const $close = event.target; const $modal = $close.closest('.modal'); // Close the modal plugin.this.close($modal); } }; /** * Click event handler to continue a modal. * @param {object} event The event object. * @return {void} */ const clickModalContinueEventHandler = (event) => { // Check if the event target is the continue or a descendant of the continue if (isTargetSelector(event.target, 'class', 'js-modal-continue')) { // Prevent the default action event.preventDefault(); // Call the continue callback plugin.settings.callbackContinue.call(); } }; /** * Click event handler to trigger a modal. * @param {object} event The event object. * @return {void} */ const clickModalTriggerEventHandler = (event) => { // Check if the event target is the trigger or a descendant of the trigger if (isTargetSelector(event.target, 'class', 'has-modal')) { // Prevent the default action event.preventDefault(); // Set the trigger and target const $trigger = event.target; const $target = document.querySelector($trigger.dataset.modalTarget); // Open the modal plugin.this.open($target); } }; /** * 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; const keycode_esc = 27; // 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; // Esc case keycode_esc: // Check if the modal can be closed with the esc key if (plugin.settings.closeEsc) { // Prevent the default action event.preventDefault(); // Call the esc callback plugin.settings.callbackEsc.call(); // Close the modal plugin.this.close($modal); } // 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 trigger a modal document.addEventListener('click', clickModalTriggerEventHandler); // Add a click event handler to close a modal document.addEventListener('click', clickModalCloseEventHandler); // Add a click event handler to cancel a modal document.addEventListener('click', clickModalCancelEventHandler); // Add a click event handler to continue a modal document.addEventListener('click', clickModalContinueEventHandler); // Check if the callbacks should not be suppressed if (!silent) { // Call the initialize after callback plugin.settings.callbackInitializeAfter.call(); } }, /** * Open a modal. * @param {element} $target The target modal. * @param {bool} silent Suppress callbacks. * @return {void} */ open: ($target, silent = false) => { // Check if the target exists and an overlay isn't already open if ($target && (!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 modal buildModal($target); // Set the modal elements const $modal = document.querySelector('.modal'); const $content = $modal.querySelector('.modal__content'); // Set the modal data $modal.data = { overlayAnimationIn: $target.dataset.overlayAnimationIn || plugin.settings.overlayAnimationIn, overlayAnimationOut: $target.dataset.overlayAnimationOut || plugin.settings.overlayAnimationOut, contentAnimationIn: $target.dataset.contentAnimationIn || plugin.settings.contentAnimationIn, contentAnimationOut: $target.dataset.contentAnimationOut || plugin.settings.contentAnimationOut, }; // Set the modal tabindex and focus on the modal $modal.setAttribute('tabindex', -1); $modal.focus(); // Trap focus inside the modal trapFocus($modal); // Check if the overlay is animated if (plugin.settings.overlayAnimation) { // Set the modal animation classes $modal.classList.add('is-animating-in', plugin.settings.overlayAnimationClass, $modal.data.overlayAnimationIn); // Add an animation end event listener to the modal $modal.addEventListener('animationend', (event) => { // Set the the modal animation classes $modal.classList.remove('is-animating-in', plugin.settings.overlayAnimationClass, $modal.data.overlayAnimationIn); $modal.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, $modal.data.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, $modal.data.contentAnimationIn); $content.classList.add('has-animated'); }, { once: true }); } } }, /** * Close a modal. * @param {element} $target The target for the modal. * @param {bool} silent Suppress callbacks. * @return {void} */ close: ($modal, silent = false) => { // Check if the modal exists and an overlay modal is open if ($modal && (document.body.classList.contains('has-overlay') || document.querySelector('.overlay.modal'))) { // Check if the callbacks should not be suppressed if (!silent) { // Call the close before callback plugin.settings.callbackCloseBefore.call(); } // Set the content const $content = $modal.querySelector('.modal__content'); // Check if the overlay is animated if (plugin.settings.overlayAnimation) { // Set the modal animation classes $modal.classList.add('is-animating-out', plugin.settings.overlayAnimationClass, $modal.data.overlayAnimationOut); // Add an animation end event listener to the modal $modal.addEventListener('animationend', (event) => { // Remove the modal $modal.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 modal $modal.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, $modal.data.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, $modal.data.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(); } // Set the modals and triggers const $modals = document.querySelectorAll('.modal'); const $triggers = document.querySelectorAll(plugin.element); // Check if any modals exists if ($modals) { // Cycle through all of the modals $modals.forEach(($modal) => { // Close the modal plugin.this.close($modal); }); } // Remove the click event handler to trigger a modal document.removeEventListener('click', clickModalTriggerEventHandler); // Remove the click event handler to close a modal document.removeEventListener('click', clickModalCloseEventHandler); // Remove the click event handler to cancel a modal document.removeEventListener('click', clickModalCancelEventHandler); // Remove the click event handler to continue a modal document.removeEventListener('click', clickModalContinueEventHandler); // Check if the callbacks should not be suppressed if (!silent) { // Call the destroy after callback plugin.settings.callbackDestroyAfter.call(); } }, /** * Call the open method silently. * @param {element} $target The target for the modal. * @return {void} */ openSilently: ($target) => { // Call the open method silently plugin.this.open($target, true); }, /** * Call the close method silently. * @param {element} $modal The modal. * @return {void} */ closeSilently: ($modal) => { // Call the close method silently plugin.this.close($modal, 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; }));