UNPKG

just-add-juice

Version:

A responsive frontend framework with modular components.

593 lines (514 loc) 22.2 kB
/* ======================================================================== JUICE -> COMPONENTS -> PROMPT ======================================================================== */ ;(function (root, factory) { // Set the plugin name const plugin_name = 'Prompt'; // 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 = { cancelButtonClass: 'button--flat', cancelButtonText: 'Cancel', center: null, color: 'primary', contentAnimation: true, contentAnimationClass: 'has-animation', contentAnimationIn: 'fade-in-up', contentAnimationOut: 'fade-out-down', continueButtonClass: null, continueButtonText: 'Continue', heading: '<h4>Heading</h4>', inputClass: null, inputRegexPattern: /\w/g, overlayAnimation: true, overlayAnimationClass: 'has-animation', overlayAnimationIn: 'fade-in', overlayAnimationOut: 'fade-out', feedback: null, size: null, text: 'Lorem ipsum...', callbackInitializeBefore: () => { console.log('Prompt: callbackInitializeBefore'); }, callbackInitializeAfter: () => { console.log('Prompt: callbackInitializeAfter'); }, callbackOpenBefore: () => { console.log('Prompt: callbackOpenBefore'); }, callbackOpenAfter: () => { console.log('Prompt: callbackOpenAfter'); }, callbackCloseBefore: () => { console.log('Prompt: callbackCloseBefore'); }, callbackCloseAfter: () => { console.log('Prompt: callbackCloseAfter'); }, callbackRefreshBefore: () => { console.log('Prompt: callbackRefreshBefore'); }, callbackRefreshAfter: () => { console.log('Prompt: callbackRefreshAfter'); }, callbackDestroyBefore: () => { console.log('Prompt: callbackDestroyBefore'); }, callbackDestroyAfter: () => { console.log('Prompt: callbackDestroyAfter') }, callbackCancel: () => { console.log('Prompt: callbackCancel'); }, callbackContinue: () => { console.log('Prompt: 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 prompt. * @return {void} */ const buildPrompt = () => { // Create the prompt const $prompt = 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 $text = document.createElement('div'); const $field = document.createElement('div'); const $input = document.createElement('input'); const $foot = document.createElement('div'); const $buttons = document.createElement('div'); const $cancel = document.createElement('button'); const $continue = document.createElement('button'); // Construct the prompt $prompt.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.append($text); $body.append($field); $field.append($input); $text.insertAdjacentHTML('beforeend', plugin.settings.text); // Construct the foot $foot.append($buttons); $buttons.append($cancel); $buttons.append($continue); $cancel.append(plugin.settings.cancelButtonText); $continue.append(plugin.settings.continueButtonText); // Add the prompt classes $prompt.classList.add('overlay', 'prompt'); $content.classList.add('prompt__content'); $card.classList.add('prompt__card'); $head.classList.add('prompt__head'); $headings.classList.add('prompt__headings'); $body.classList.add('prompt__body'); $text.classList.add('prompt__text'); $field.classList.add('prompt__field'); $input.classList.add('prompt__input', plugin.settings.inputClass); $foot.classList.add('prompt__foot'); $buttons.classList.add('prompt__buttons'); $cancel.classList.add(plugin.settings.cancelButtonClass, 'js-prompt-cancel'); $continue.classList.add(plugin.settings.continueButtonClass, 'js-prompt-continue'); // Check if a center modifier exists if (plugin.settings.center) { // Add the center modifier class to the prompt $prompt.classList.add('prompt--center'); } // Check if a size modifier exists if (plugin.settings.size) { // Add the size modifier class to the prompt $prompt.classList.add(`is-${plugin.settings.size}`); $input.classList.add(`is-${plugin.settings.size}`); $cancel.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 prompt $prompt.classList.add(`is-${plugin.settings.color}`); $cancel.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 prompt $prompt.classList.add(`has-${plugin.settings.feedback}`); $cancel.classList.add(`has-${plugin.settings.feedback}`); $continue.classList.add(`has-${plugin.settings.feedback}`); } // Append the prompt to the document body document.body.insertAdjacentHTML('beforeend', $prompt.outerHTML); }; /** * Click event handler to cancel a prompt. * @param {object} event The event object. * @return {void} */ const clickPromptCancelEventHandler = (event) => { // Check if the event target is the cancel or a descendant of the cancel if (isTargetSelector(event.target, 'class', 'js-prompt-cancel')) { // Prevent the default action event.preventDefault(); // Call the cancel callback plugin.settings.callbackCancel.call(); } }; /** * Click event handler to continue a prompt. * @param {object} event The event object. * @return {void} */ const clickPromptContinueEventHandler = (event) => { // Check if the event target is the continue or a descendant of the continue if (isTargetSelector(event.target, 'class', 'js-prompt-continue')) { // Prevent the default action event.preventDefault(); // Set the prompt and input const $prompt = plugin.this.prompt; const $input = $prompt.querySelector('.prompt__input'); // Set the result const result = plugin.settings.inputRegexPattern.test($input.value); // Call the continue callback plugin.settings.callbackContinue.call(this, $input, result); } }; /** * 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 cancel a prompt document.addEventListener('click', clickPromptCancelEventHandler); // Add a click event handler to continue a prompt document.addEventListener('click', clickPromptContinueEventHandler); // Check if the callbacks should not be suppressed if (!silent) { // Call the initialize after callback plugin.settings.callbackInitializeAfter.call(); } // Open the prompt and break the switch plugin.this.open(); }, /** * Open the prompt. * @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 prompt buildPrompt(); // Set the prompt elements const $prompt = document.querySelector('.prompt'); const $content = $prompt.querySelector('.prompt__content'); // Set the plugin prompt plugin.this.prompt = $prompt; // Set the prompt tabindex and focus on the prompt $prompt.setAttribute('tabindex', -1); $prompt.focus(); // Trap focus inside the prompt trapFocus($prompt); // Check if the overlay is animated if (plugin.settings.overlayAnimation) { // Set the prompt animation classes $prompt.classList.add('is-animating-in', plugin.settings.overlayAnimationClass, plugin.settings.overlayAnimationIn); // Add an animation end event listener to the prompt $prompt.addEventListener('animationend', (event) => { // Set the the prompt animation classes $prompt.classList.remove('is-animating-in', plugin.settings.overlayAnimationClass, plugin.settings.overlayAnimationIn); $prompt.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 prompt. * @param {bool} silent Suppress callbacks. * @return {void} */ close: (silent = false) => { // Set the prompt const $prompt = plugin.this.prompt; // Check if the prompt exists and an overlay prompt is open if ($prompt && (document.body.classList.contains('has-overlay') || document.querySelector('.overlay.prompt'))) { // Check if the callbacks should not be suppressed if (!silent) { // Call the close before callback plugin.settings.callbackCloseBefore.call(); } // Set the content const $content = $prompt.querySelector('.prompt__content'); // Check if the overlay is animated if (plugin.settings.overlayAnimation) { // Set the prompt animation classes $prompt.classList.add('is-animating-out', plugin.settings.overlayAnimationClass, plugin.settings.overlayAnimationOut); // Add an animation end event listener to the prompt $prompt.addEventListener('animationend', (event) => { // Remove the prompt $prompt.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 prompt $prompt.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 cancel a prompt document.removeEventListener('click', clickPromptCancelEventHandler); // Remove the click event handler to continue a prompt document.removeEventListener('click', clickPromptContinueEventHandler); // 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; }));