UNPKG

focus-visibility

Version:

Front end visibility manager for popups, modals notifications, drawers, accordions and tabs

325 lines (291 loc) 10.6 kB
/* ===================================================================================== @preserve = Focus version v2.1.7 Author: George Butter - github.com/ButsAndCats ISC License ================================================================================================= */ /* Manages visibility for popups, drawers, modals, notifications, tabs, accordions and anything else. */ const Focus = function focusVisibilityManager(target, config) { const settings = config || {}; // The default configuration /* visibleClass: The class that will be applied to the target element. bodyClass: The class that will be applied to the body. triggerClass: The class that will be applied to the trigger that is clicked on. detach: if the body should be appended to the body. innerSelector: The outer area of the element, acts like a close button when clicked. autoFocusSelector: An input field that you would like to be focused with the element opens. slide: Whether the opening should be animated with javascript, useful for accordions. slideDuration: Speed of the animation, can be defined in ms. visible: Whether the element was loaded visible or not. success: Callback for when an element is success fully made visible. error: Callback fro when an element could not be made visible. */ const defaultSettings = { visibleClass: 'visible', bodyClass: 'active-popup', triggerClass: null, detach: null, innerSelector: '.popup-inner', autoFocusSelector: '[data-auto-focus]', slide: null, slideDuration: 'fast', visible: false, showCallback: null, hideCallback: null, error: null, }; // Merge configs this.settings = Object.assign(defaultSettings, settings); // Update current popup config this.visible = this.settings.visible; // The Dom element of the popup this.element = document.querySelector(target); this.innerElement = document.querySelector(`${target} ${this.settings.innerSelector}`); this.closeElement = document.querySelector(`${target} [data-close]`); this.target = target; if (this.settings.slide) { const defaultDisplay = this.element.style.display; this.element.style.display = 'block'; this.maxHeight = this.element.offsetHeight; this.element.style.display = defaultDisplay; this.height = this.element.offsetHeight; this.counter = this.height; } // Bind this into all of our prototype functions this.show = this.show.bind(this); this.hide = this.hide.bind(this); this.toggle = this.toggle.bind(this); this.detach = this.detach.bind(this); this.slideDown = this.slideDown.bind(this); this.slideUp = this.slideUp.bind(this); // If detach is set to true move the popup to the end of the popup if (this.settings.detach) { document.addEventListener('DOMContentLoaded', this.detach); } // Create a list of all of the currently active elements so that we can access them globally Focus.elements[target] = this; }; // Create an empty object to store all of the elements in. Focus.elements = {}; // Prevent default if the element is a link and return the selector of the popup element Focus.getTarget = function getTheFocusElementRelatedToTheTarget(event) { const element = event.target; if (element.tagName === 'A') { event.preventDefault(); } const selector = element.dataset.target; const target = selector || null; return target; }; /* If the element does not exist then it is being fired directly from a data attribute. Therefore we create a new focus element. Then we toggle the elements visibility. */ Focus.eventHandler = function hideOrShowTheElement(event, target, method) { let focus = Focus.elements[target]; if (!focus) { focus = new Focus(target); } if (method === 'hide') { return focus.hide(); } return focus.toggle(); }; /* When clicking on a close button or out element */ Focus.closeEvent = function handleAnElementBeingClosed(event) { const target = Focus.getTarget(event); if (target) { Focus.eventHandler(event, target, 'hide'); } else { this.hide(); } }; /* On key up event check if the user has pressed escape */ Focus.escEvent = function onKeyUpEscape(event) { if (event.keyCode === 27) { this.element.removeEventListener('keyup', Focus.escEvent); this.hide(); } }; /* Build the event listeners */ Focus.buildEventListeners = function bindFocusEventListeners() { const allTriggers = document.querySelectorAll('[data-trigger]'); for (let trigger = 0; trigger < allTriggers.length; trigger += 1) { const { target } = allTriggers[trigger].dataset; allTriggers[trigger].addEventListener('click', (event) => { event.preventDefault(); Focus.eventHandler(event, target); }); } }; /* Add a class to a given element */ Focus.addClass = function addAClassToAGivenElement(element, className) { const el = element; if (el.classList) { el.classList.add(className); } }; /* Remove a class from a given element */ Focus.removeClass = function removeAClassFromAGivenElement(element, className) { const el = element; if (el.classList) { el.classList.remove(className); } }; /* Show the popup element */ Focus.prototype.show = function showTheElement() { // Check if the element is visible or not. if (!this.visible || !this.element.classList.contains(this.settings.visibleClass)) { // Add the class to the trigger button if one is defined. if (this.settings.triggerClass) { const triggerElement = document.querySelector(`[data-target="${this.target}"]`); Focus.addClass(triggerElement, this.settings.visibleClass); } // If slide is set to true slide the element down. if (this.settings.slide) { this.slideDown(this.settings.slideDuration); } // Add the visible class to the popup Focus.addClass(this.element, this.settings.visibleClass); // Add the body class to the body Focus.addClass(document.body, this.settings.bodyClass); // Define that this element is visible this.visible = true; // Focus on an input field once the modal has opened const focusEl = document.querySelector(`${this.target} ${this.settings.autoFocusSelector}`); if (focusEl) { setTimeout(() => { focusEl.focus(); }, 300); } if (this.closeElement) { // When someone clicks the [data-close] button then we should close the modal this.closeElement.addEventListener('click', Focus.closeEvent.bind(this)); } if (this.innerElement) { // When someone clicks on the inner class hide the popup this.innerElement.addEventListener('click', Focus.closeEvent.bind(this)); } // When someone presses esc hide the popup and unbind the event listener this.element.addEventListener('keyup', Focus.escEvent.bind(this)); // Fire the success callback if (this.settings.showCallback && typeof this.settings.showCallback === 'function') { this.settings.showCallback(this); } } else if (this.settings.error && typeof this.settings.error === 'function') { this.settings.error('Focus: Error this element is already visible', this); } // Return this so that we can chain functions together return this; }; Focus.prototype.slideDown = function slideDown() { const el = this.element; // Display none const defaultDisplay = this.element.style.display; el.style.display = 'block'; el.style.overflow = 'visible'; el.style.maxHeight = '100%'; // Declare the value of "height" variable this.maxHeight = el.offsetHeight; el.style.display = defaultDisplay; this.height = el.offsetHeight; // Declare the value of "counter" variable this.counter = this.height; el.style.maxHeight = `${this.height}px`; el.style.overflow = 'hidden'; el.style.display = 'block'; const adder = this.maxHeight / 100; // Iteratively increase the height this.interval = setInterval(() => { this.counter += adder; if (this.counter < this.maxHeight) { el.style.maxHeight = `${this.counter}px`; } else { el.style.maxHeight = null; el.style.overflow = null; this.height = this.element.offsetHeight; clearInterval(this.interval); } }, this.settings.slideSpeed / 100); }; Focus.prototype.slideUp = function slideUp() { const el = this.element; const subtractor = this.maxHeight / 100; // To hide the content of the element el.style.overflow = 'hidden'; // Decreasing the height this.interval = setInterval(() => { this.counter -= subtractor; if (this.counter > 0) { el.style.maxHeight = `${this.counter}px`; } else { el.style.maxHeight = null; el.style.display = 'none'; el.style.overflow = null; clearInterval(this.interval); } }, this.settings.slideSpeed / 100); }; Focus.prototype.hide = function hideTheElement() { if (this.visible || this.element.classList.contains(this.settings.visibleClass)) { Focus.removeClass(this.element, this.settings.visibleClass); if (this.settings.triggerClass) { const triggerElement = document.querySelector(`[data-target="${this.target}"]`); Focus.removeClass(triggerElement, this.settings.visibleClass); } if (this.settings.slide) { this.slideUp(this.settings.slideDuration); } Focus.removeClass(document.body, this.settings.bodyClass); if (this.closeElement) { // When someone clicks the [data-close] button then we should close the modal this.closeElement.removeEventListener('click', Focus.closeEvent); } if (this.innerElement) { // When someone clicks on the inner class hide the popup this.innerElement.removeEventListener('click', Focus.closeEvent); } this.visible = false; // Fire the success callback if (this.settings.hideCallback && typeof this.settings.hideCallback === 'function') { this.settings.hideCallback(this); } } else if (this.settings.error && typeof this.settings.error === 'function') { this.settings.error('Focus: Error this element is already hidden', this); } return this; }; /* Show if hidden, hide if shown. */ Focus.prototype.toggle = function toggleFocusVisibility() { if (this.visible) { this.hide(); } else { this.show(); } return this; }; /* Move the element to the end of the body, sometime useful for popups. */ Focus.prototype.detach = function moveTheElementToTheEndOfTheBody() { document.body.appendChild(this.element); return this; }; export {Focus};