UNPKG

just-add-juice

Version:

A responsive frontend framework with modular components.

610 lines (527 loc) 21.8 kB
/* ======================================================================== JUICE -> COMPONENTS -> TOOLTIP ======================================================================== */ ;(function (root, factory) { // Set the plugin name const plugin_name = 'Tooltip'; // 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: 'zoom-in', animationOut: 'zoom-out', color: null, delayIn: 0, delayOut: 0, feedback: null, position: 'top', size: null, callbackInitializeBefore: () => { console.log('Tooltip: callbackInitializeBefore'); }, callbackInitializeAfter: () => { console.log('Tooltip: callbackInitializeAfter'); }, callbackOpenBefore: () => { console.log('Tooltip: callbackOpenBefore'); }, callbackOpenAfter: () => { console.log('Tooltip: callbackOpenAfter'); }, callbackCloseBefore: () => { console.log('Tooltip: callbackCloseBefore'); }, callbackCloseAfter: () => { console.log('Tooltip: callbackCloseAfter'); }, callbackRefreshBefore: () => { console.log('Tooltip: callbackRefreshBefore'); }, callbackRefreshAfter: () => { console.log('Tooltip: callbackRefreshAfter'); }, callbackDestroyBefore: () => { console.log('Tooltip: callbackDestroyBefore'); }, callbackDestroyAfter: () => { console.log('Tooltip: 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(); } /** * Build the tooltip. * @param {element} $trigger The tooltip trigger. * @return {element} The tooltip. */ const buildTooltip = ($trigger) => { // Create the tooltip and content const $tooltip = document.createElement('div'); const $content = document.createElement('div'); // Add the tooltip and content classes $tooltip.classList.add('tooltip'); $content.classList.add('tooltip__content'); // Construct the tooltip $tooltip.append($content); $content.append($trigger.dataset.tooltip); // Set the tooltip modifiers const color = $trigger.dataset.tooltipColor || plugin.settings.color; const feedback = $trigger.dataset.tooltipFeedback || plugin.settings.feedback; const position = $trigger.dataset.tooltipPosition || plugin.settings.position; const size = $trigger.dataset.tooltipSize || plugin.settings.size; // Check if a color modifier exists if (color) { // Add the color modifier class to the tooltip $tooltip.classList.add(`is-${color}`); } // Check if a feedback modifier exists if (feedback) { // Add the feedback modifier class to the tooltip $tooltip.classList.add(`has-${feedback}`); } // Check if a position modifier exists if (position) { // Add the position modifier class to the tooltip $tooltip.classList.add(`tooltip--${position}`); } // Check if a size modifier exists if (size) { // Add the size modifier class to the tooltip $tooltip.classList.add(`is-${size}`); } // Set the tooltip animations const animation_in = $trigger.dataset.tooltipAnimationIn || plugin.settings.animationIn; const animation_out = $trigger.dataset.tooltipAnimationOut || plugin.settings.animationOut; // Set the tooltip delay const delay_in = $trigger.dataset.tooltipDelayIn || plugin.settings.delayIn; const delay_out = $trigger.dataset.tooltipDelayOut || plugin.settings.delayOut; // Set the tooltip data $tooltip.data = { animationIn: animation_in, animationOut: animation_out, delayIn: delay_in, delayOut: delay_out }; // Return the tooltip return $tooltip; }; /** * Event handler to close a tooltip. * @param {object} event The event object. * @return {void} */ const closeTooltipEventHandler = (event) => { // Check if the event target is the trigger or a descendant of the trigger if (isTargetSelector(event.target, 'class', 'has-tooltip')) { // Set the trigger and tooltip const $trigger = event.target; const $tooltip = $trigger.data.tooltip; // Close the tooltip closeTooltip($tooltip); } }; /** * Get an elements offset. * @param {element} $element The element. * @return {object} The element offset. */ const getElementOffset = ($element) => { // Set the top and left positions let left = 0; let top = 0; // Loop through the dom tree and calculate each parent offset do { // Set the top and left positions top += $element.offsetTop || 0; left += $element.offsetLeft || 0; // Set the element to the parent element $element = $element.offsetParent; } while ($element); // Return the top and left positions return { top, left }; }; /** * 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}`)); } }; /** * Event handler to open a tooltip relative to its tooltip trigger. * @param {object} event The event object. * @return {void} */ const openTooltipEventHandler = (event) => { // Check if the event target is the trigger or a descendant of the trigger if (isTargetSelector(event.target, 'class', 'has-tooltip')) { console.log(event.type); // Set the tooltip trigger const $trigger = event.target; // Open the tooltip openTooltip($trigger); } }; /** * Set the tooltip position relative to the tooltip trigger. * @param {element} $trigger The tooltip trigger. * @param {element} $tooltip The tooltip. * @return {void} */ const positionTooltip = ($trigger, $tooltip) => { // Set the tooltip trigger offset const trigger_offset = getElementOffset($trigger); // Set the tooltip trigger left and top positions const trigger_left = trigger_offset.left; const trigger_top = trigger_offset.top; // Set the tooltip trigger dimensions const trigger_width = $trigger.offsetWidth; const trigger_height = $trigger.offsetHeight; // Set the tooltip dimensions const tooltip_width = $tooltip.offsetWidth; const tooltip_height = $tooltip.offsetHeight; // Set the tooltip left and top positions let tooltip_left; let tooltip_top; // Set the tooltip position modifier const position = $trigger.dataset.tooltipPosition || plugin.settings.position; // Start a switch statement for the tooltip position switch (position) { // Top (default) default: // Set the tooltip left and top positions tooltip_left = trigger_left + ((trigger_width - tooltip_width) / 2); tooltip_top = trigger_top - tooltip_height; break; // Right case 'right': // Set the tooltip left and top positions tooltip_left = trigger_left + trigger_width; tooltip_top = trigger_top + ((trigger_height - tooltip_height) / 2); break; // Bottom case 'bottom': // Set the tooltip left and top positions tooltip_left = trigger_left + ((trigger_width - tooltip_width) / 2); tooltip_top = trigger_top + trigger_height; break; // Left case 'left': // Set the tooltip left and top positions tooltip_left = trigger_left - tooltip_width; tooltip_top = trigger_top + ((trigger_height - tooltip_height) / 2); break; } // Set the inline top and left positions $tooltip.style.left = `${Math.round(tooltip_left)}px`; $tooltip.style.top = `${Math.round(tooltip_top)}px`; }; /** * Open a tooltip. * @param {element} $trigger The trigger. * @return {void} */ const openTooltip = ($trigger, silent = false) => { // Check if the trigger is not active if (!$trigger.classList.contains('is-active')) { // Check if the callbacks should not be suppressed if (!silent) { // Call the open before callback plugin.settings.callbackOpenBefore.call(silent); } // Add the active state hook to the tooltip trigger $trigger.classList.add('is-active'); // // Set the tooltips // const $tooltips = document.querySelectorAll('.tooltip:not(.is-animating-out)'); // // // Check if any tooltips exist // if ($tooltips) { // // Cycle through all of the tooltips // $tooltips.forEach(($tooltip) => { // // Close the tooltip // plugin.this.close($tooltip); // }); // } // Set the tooltip const $tooltip = buildTooltip($trigger); // Check if the trigger has a data object if ($trigger.data) { // Assign the tooltip to the trigger data object $trigger.data['tooltip'] = $tooltip; } else { // Assign the tooltip to the trigger data object $trigger.data = { tooltip: $tooltip }; } // Assign the trigger to the tooltip data object $tooltip.data['trigger'] = $trigger; // Show the tooltip $tooltip.style.display = 'block'; // Append the tooltip to the body document.body.appendChild($tooltip); // Position the tooltip positionTooltip($trigger, $tooltip); // Start a timer setTimeout(() => { // Show the tooltip $tooltip.style.display = 'block'; // Check if the the tooltip is animated if (plugin.settings.animation) { // Set the tooltip animation classes $tooltip.classList.add('is-animating-in', plugin.settings.animationClass, $tooltip.data.animationIn); // Add an animation end event listener to the tooltip $tooltip.addEventListener('animationend', (event) => { // Set the tooltip animation classes $tooltip.classList.remove('is-animating-in', plugin.settings.animationClass, $tooltip.data.animationIn); $tooltip.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(); } } }, $tooltip.data.delayIn); } }; /** * Close a tooltip. * @param {element} $tooltip The tooltip. * @param {bool} silent Suppress callbacks. * @return {void} */ const closeTooltip = ($tooltip, silent = false) => { // Check if the tooltip exists and is not animating out if (!$tooltip.classList.contains('is-animating-out')) { // Check if the callbacks should not be suppressed if (!silent) { // Call the remove before callback plugin.settings.callbackCloseBefore.call(); } // Set the trigger const $trigger = $tooltip.data.trigger; // Remove the active state hook from the tooltip trigger $trigger.classList.remove('is-active'); // Start a timer setTimeout(() => { // Check if the the tooltip is animated if (plugin.settings.animation) { // Set the tooltip animation classes $tooltip.classList.remove('has-animated'); $tooltip.classList.add('is-animating-out', plugin.settings.animationClass, $tooltip.data.animationOut); // Add an animation end event listener to the tooltip $tooltip.addEventListener('animationend', (event) => { // Check if the tooltip exists if ($tooltip) { // Remove the tooltip $tooltip.remove(); } // Remove the assigned tooltip and timer from the trigger data delete $trigger.data['tooltip']; delete $trigger.data['timer']; // Check if the callbacks should not be suppressed if (!silent) { // Call the remove after callback plugin.settings.callbackCloseAfter.call(); } }, { once: true }); } else { // Check if the tooltip exists if ($tooltip) { // Remove the tooltip $tooltip.remove(); } // Remove the assigned tooltip from the trigger data delete $trigger.data['tooltip']; // Check if the callbacks should not be suppressed if (!silent) { // Call the remove after callback plugin.settings.callbackCloseAfter.call(); } } }, $tooltip.data.delayOut); } }; /** * 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(); } // Check if the device is not a touch device if (document.documentElement.classList.contains('has-no-touch')) { // Add focus and mouseenter event handler to open a tooltip document.addEventListener('focus', openTooltipEventHandler, true); document.addEventListener('mouseenter', openTooltipEventHandler, true); // Add click, focusout and mouseleave event handlers to close a tooltip document.addEventListener('click', closeTooltipEventHandler); document.addEventListener('focusout', closeTooltipEventHandler, true); document.addEventListener('mouseleave', closeTooltipEventHandler, true); } // Check if the callbacks should not be suppressed if (!silent) { // Call the initialize after callback plugin.settings.callbackInitializeAfter.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(); } // Check if the device is not a touch device if (document.documentElement.classList.contains('has-no-touch')) { // Set the tooltips const $tooltips = document.querySelectorAll('.tooltip'); // Cycle through all of the tooltips $tooltips.forEach(($tooltip) => { // Close the tooltip plugin.this.close($tooltip); }); // Remove the focus and mouseenter event handler to open a tooltip document.removeEventListener('focus', openTooltipEventHandler, true); document.removeEventListener('mouseenter', openTooltipEventHandler, true); // Remove the click, focusout and mouseleave event handlers to close a tooltip document.removeEventListener('click', closeTooltipEventHandler); document.removeEventListener('focusout', closeTooltipEventHandler, true); document.removeEventListener('mouseleave', closeTooltipEventHandler, true); } // Check if the callbacks should not be suppressed. if (!silent) { // Call the destroy after callback plugin.settings.callbackDestroyAfter.call(); } }, /** * 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; }));