/**
|
* 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;
|
});
|