/** * Shortcut class to centralize all modal functionality, utilized by the JS ecosystem */ class ModalController { constructor(modal, options){ this.modal = modal; this.a11y = window.jvbA11y; this.options = { openMessage: 'Opened modal', closeMessage: 'Closed modal', open: null, close: 'button.cancel', save: 'button[type="submit"]', ...options } this.subscribers = new Set(); if (!ModalController.modalStack) { ModalController.modalStack = []; } this.boundEscapeListener = this.handleEscapeListener.bind(this); this.boundBackdropListener = this.handleBackdropListener.bind(this); this.init(); } init(){ this.isOpen = false; this.hasChanges = false; this.initElements(); this.initEvents(); } initElements() { this.elements = { open: this.options.open, close: this.options.close, save: this.options.save, }; } handleClose(){ // Only close if this is the topmost modal if (ModalController.modalStack[ModalController.modalStack.length - 1] !== this) { return; } this.notify('modal-close', this.modal); this.a11y.announce(this.options.closeMessage); this.modal.close(); this.isOpen = false; // Remove this modal from the stack const index = ModalController.modalStack.indexOf(this); if (index !== -1) { ModalController.modalStack.splice(index, 1); } this.showBody(); // Clean up event listeners this.removeCloseListeners(); } handleOpen(e){ this.addCloseListeners(); this.hideBody(); this.isOpen = true; this.modal.showModal(); if (!this.a11y) { this.a11y = window.jvbA11y; } this.a11y.trapFocus(this.modal); this.a11y.announce(this.options.openMessage); // Add this modal to the stack ModalController.modalStack.push(this); this.notify('modal-open', {modal: this.modal, event: e}); } addCloseListeners() { document.addEventListener('keydown', this.boundEscapeListener); document.addEventListener('click', this.boundBackdropListener); } removeCloseListeners() { document.removeEventListener('keydown', this.boundEscapeListener); document.removeEventListener('click', this.boundBackdropListener); } handleEscapeListener(e) { if (e.key === 'Escape' && ModalController.modalStack[ModalController.modalStack.length - 1] === this) { e.preventDefault(); // Prevent default browser behavior this.handleClose(); } } handleBackdropListener(e) { if (e.target === this.modal && ModalController.modalStack[ModalController.modalStack.length - 1] === this) { this.handleClose(); } } hideBody(){ // Only hide body if this is the first modal if (ModalController.modalStack.length === 0) { document.body.style.overflow = 'hidden'; } } showBody(){ if (ModalController.modalStack.length === 0) { document.body.style.overflow = ''; } } initEvents(){ document.addEventListener('click', this.handleClick.bind(this)); // document.addEventListener('beforeUnload', () => this.destroy()); } handleClick(e) { // Handle open triggers from anywhere if (this.elements.open && window.targetCheck(e, this.elements.open)) { this.handleOpen(e); return; } // Only handle close/save if the click is within this modal if (!this.modal.contains(e.target)) { return; } // if (this.elements.save && window.targetCheck(e, this.elements.save)) { // this.handleClose(); // } else if (this.elements.close && window.targetCheck(e, this.elements.close)) { // Additional check: only close if we're the top modal if (ModalController.modalStack[ModalController.modalStack.length - 1] === this) { this.handleClose(); } } } // Static methods for managing modal stack static getTopModal() { return ModalController.modalStack[ModalController.modalStack.length - 1] || null; } static getAllModals() { return [...ModalController.modalStack]; } static closeTopModal() { const topModal = ModalController.getTopModal(); if (topModal) { topModal.handleClose(); } } static closeAllModals() { // Close from top to bottom to maintain proper stack behavior while (ModalController.modalStack.length > 0) { ModalController.closeTopModal(); } } /** * Event system */ subscribe(callback) { this.subscribers.add(callback); return () => this.subscribers.delete(callback); } notify(event, data) { this.subscribers.forEach(cb => cb(event, data)); } destroy() { this.subscribers.clear(); ModalController.closeAllModals(); this.showBody(); this.removeCloseListeners(); } } document.addEventListener('DOMContentLoaded', ()=> { window.jvbModal = ModalController; });