UNPKG

modalitor

Version:

A modern, accessible modal system with focus management, URL hash support, and rich animations

8 lines 7.48 kB
/** * @name Modalitor * @author HamidReza Yazdani * @license MIT * @version 1.0.3 * @description A modern, accessible modal system with focus management, URL hash support, and rich animations */ const ANIMATION_DURATIONS={DEFAULT:300,BOUNCE:800,DOOR:600,ROTATE:600,ROADRUNNER:800,FLIP:600,UNFOLD:600};class Modalitor{static instances=new Map;static zIndex=1e3;static activeModal=null;static scrollPosition=null;static scrollbarWidth=null;static modalCount=0;constructor(t){if(!t.id)throw new Error("Modal ID is required");const i=Modalitor.instances.get(t.id);if(i)return i;null===Modalitor.scrollbarWidth&&(Modalitor.scrollbarWidth=this.calculateScrollbarWidth()),this.defaultOptions={size:"md",animation:"default",closeOnOverlay:!0,closeOnEscape:!0,preventScroll:!0,role:"dialog",labelledBy:null,describedBy:null,updateUrl:!0,hashPrefix:"modal-",onShow:()=>{},onHide:()=>{}},this.options={...this.defaultOptions,...t},this.focusedElementBeforeModal=null,this.isClosing=!1,this.initializeModal(),Modalitor.instances.set(this.options.id,this),this.handleInitialHash()}calculateScrollbarWidth(){const t=document.createElement("div");t.style.visibility="hidden",t.style.overflow="scroll",document.body.appendChild(t);const i=document.createElement("div");t.appendChild(i);const e=t.offsetWidth-i.offsetWidth;return document.body.removeChild(t),e}initializeModal(){this.createOverlay(),this.setupModal(),this.setupEventListeners(),this.setupFocusTrap(),this.setupHashChange()}createOverlay(){if(this.overlay=document.createElement("div"),this.overlay.className="modalitor-overlay",this.overlay.setAttribute("tabindex","-1"),this.overlay.id=`modalitor-overlay-${this.options.id}`,this.modal=document.getElementById(this.options.id),!this.modal)throw new Error(`Modal with ID ${this.options.id} not found`);this.modal.parentNode?.insertBefore(this.overlay,this.modal)}setupModal(){const{role:t,labelledBy:i,describedBy:e,size:o,animation:s}=this.options;this.modal.setAttribute("role",t),this.modal.setAttribute("aria-modal","true"),this.modal.setAttribute("tabindex","-1"),i&&this.modal.setAttribute("aria-labelledby",i),e&&this.modal.setAttribute("aria-describedby",e),this.modal.classList.add("modalitor",`modalitor-${o}`),"none"!==s&&this.modal.classList.add(`animation-${s}`)}setupEventListeners(){this.setupCloseButton(),this.setupOverlayClick(),this.setupEscapeKey(),this.setupPopState()}setupCloseButton(){const t=this.modal.querySelector(".modalitor-close");t&&(t.setAttribute("aria-label","Close modal"),t.addEventListener("click",(()=>this.hide())))}setupOverlayClick(){this.options.closeOnOverlay&&this.overlay.addEventListener("click",(t=>{t.target!==this.overlay||this.isClosing||this.hide()}))}setupEscapeKey(){this.options.closeOnEscape&&document.addEventListener("keydown",(t=>{"Escape"===t.key&&this.isVisible()&&this.isTopModal()&&this.hide()}))}setupFocusTrap(){this.modal.addEventListener("keydown",(t=>{if("Tab"!==t.key)return;const i=this.getFocusableElements();if(!i.length)return;const e=i[0],o=i[i.length-1],s=document.activeElement;t.shiftKey&&s===e?(t.preventDefault(),o.focus()):t.shiftKey||s!==o||(t.preventDefault(),e.focus())}))}setupHashChange(){window.addEventListener("hashchange",(()=>{this.handleHashChange()}))}setupPopState(){window.addEventListener("popstate",(()=>{this.handleHashChange()}))}handleInitialHash(){this.shouldShowBasedOnHash()&&this.show()}shouldShowBasedOnHash(){return window.location.hash.slice(1)===`${this.options.hashPrefix}${this.options.id}`}handleHashChange(){const t=this.shouldShowBasedOnHash();t&&!this.isVisible()?this.show():!t&&this.isVisible()&&this.hide()}updateUrlHash(t){if(!this.options.updateUrl)return;const i=window.location.hash,e=`#${this.options.hashPrefix}${this.options.id}`;t&&i!==e?window.history.pushState({},"",e):t||i!==e||window.history.pushState({},"",window.location.pathname+window.location.search)}saveScrollPosition(){Modalitor.scrollPosition=window.scrollY}handleScrollLock(t=!1){this.options.preventScroll&&(t?(Modalitor.modalCount--,0===Modalitor.modalCount&&(document.body.style.overflow="",document.body.style.paddingRight="",null!==Modalitor.scrollPosition&&(window.scrollTo(0,Modalitor.scrollPosition),Modalitor.scrollPosition=null))):(0===Modalitor.modalCount&&(Modalitor.scrollPosition=window.scrollY,document.body.style.overflow="hidden",document.body.style.paddingRight=`${Modalitor.scrollbarWidth}px`),Modalitor.modalCount++))}show(){this.isVisible()||(this.focusedElementBeforeModal=document.activeElement,this.updateZIndex(),0===Modalitor.modalCount&&this.saveScrollPosition(),this.updateUrlHash(!0),this.showModalWithAnimation(),this.handleScrollLock(),this.updateAriaAttributes(!1),Modalitor.activeModal=this,this.options.onShow())}hide(){this.isVisible()&&!this.isClosing&&(this.isClosing=!0,this.modal.classList.add("closing"),this.overlay.classList.remove("active"),this.updateUrlHash(!1),setTimeout((()=>{this.completeHiding(),this.isClosing=!1,this.options.onHide()}),this.getAnimationDuration()))}completeHiding(){this.modal.classList.remove("active","closing"),this.restoreFocus(),this.handleScrollLock(!0),this.updateAriaAttributes(!0),Modalitor.activeModal===this&&(Modalitor.activeModal=null)}updateZIndex(){Modalitor.zIndex+=2,this.overlay.style.zIndex=String(Modalitor.zIndex),this.modal.style.zIndex=String(Modalitor.zIndex+1)}showModalWithAnimation(){requestAnimationFrame((()=>{this.overlay.classList.add("active"),this.modal.classList.add("active"),this.modal.focus()}))}getFocusableElements(){return Array.from(this.modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))}updateAriaAttributes(t){this.modal.setAttribute("aria-hidden",String(t))}restoreFocus(){this.focusedElementBeforeModal&&this.focusedElementBeforeModal.focus()}getAnimationDuration(){return ANIMATION_DURATIONS[this.options.animation.toUpperCase()]||ANIMATION_DURATIONS.DEFAULT}isTopModal(){return Array.from(Modalitor.instances.values()).filter((t=>t.isVisible())).sort(((t,i)=>parseInt(i.modal.style.zIndex||"0")-parseInt(t.modal.style.zIndex||"0")))[0]===this}isVisible(){return this.modal.classList.contains("active")}destroy(){this.isVisible()&&this.hide(),this.overlay.remove(),Modalitor.instances.delete(this.options.id),this.isVisible()&&(Modalitor.modalCount=Math.max(0,Modalitor.modalCount-1))}static getAllInstances(){return Modalitor.instances}static handlePageLoad(){const t=window.location.hash.slice(1);if(!t)return;const i="modal-";if(t.startsWith(i)){const e=t.slice(i.length),o=Modalitor.instances.get(e);if(o)o.show();else{if(document.getElementById(e)){new Modalitor({id:e}).show()}}}}}document.addEventListener("click",(t=>{const i=t.target.closest("[data-modal-target]");if(!i)return;const e=i.getAttribute("data-modal-target");if(!e)return;const o={id:e,size:i.getAttribute("data-modal-size")||"md",animation:i.getAttribute("data-modal-animation")||"default",labelledBy:i.getAttribute("data-modal-label"),describedBy:i.getAttribute("data-modal-description"),role:i.getAttribute("data-modal-role")||"dialog",updateUrl:"false"!==i.getAttribute("data-modal-update-url"),hashPrefix:i.getAttribute("data-modal-hash-prefix")||"modal-"};new Modalitor(o).show()})),document.addEventListener("DOMContentLoaded",(()=>{Modalitor.handlePageLoad()})),"undefined"!=typeof module&&module.exports?module.exports=Modalitor:"function"==typeof define&&define.amd?define([],(function(){return Modalitor})):window.Modalitor=Modalitor;