|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class Modal { |
|
|
constructor(options = {}) { |
|
|
this.id = options.id || `modal-${Date.now()}`; |
|
|
this.title = options.title || ''; |
|
|
this.content = options.content || ''; |
|
|
this.size = options.size || 'medium'; |
|
|
this.closeOnBackdrop = options.closeOnBackdrop !== false; |
|
|
this.closeOnEscape = options.closeOnEscape !== false; |
|
|
this.onClose = options.onClose || null; |
|
|
this.element = null; |
|
|
this.backdrop = null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
show() { |
|
|
if (this.element) { |
|
|
console.warn('[Modal] Modal already open'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
this.backdrop = document.createElement('div'); |
|
|
this.backdrop.className = 'modal-backdrop'; |
|
|
if (this.closeOnBackdrop) { |
|
|
this.backdrop.addEventListener('click', () => this.hide()); |
|
|
} |
|
|
|
|
|
|
|
|
this.element = document.createElement('div'); |
|
|
this.element.className = `modal modal-${this.size}`; |
|
|
this.element.setAttribute('role', 'dialog'); |
|
|
this.element.setAttribute('aria-modal', 'true'); |
|
|
this.element.setAttribute('aria-labelledby', `${this.id}-title`); |
|
|
|
|
|
this.element.innerHTML = ` |
|
|
<div class="modal-dialog"> |
|
|
<div class="modal-header"> |
|
|
<h2 class="modal-title" id="${this.id}-title">${this.escapeHtml(this.title)}</h2> |
|
|
<button class="modal-close" aria-label="Close modal">×</button> |
|
|
</div> |
|
|
<div class="modal-body"> |
|
|
${this.content} |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
|
|
|
const closeBtn = this.element.querySelector('.modal-close'); |
|
|
closeBtn.addEventListener('click', () => this.hide()); |
|
|
|
|
|
|
|
|
if (this.closeOnEscape) { |
|
|
this.escapeHandler = (e) => { |
|
|
if (e.key === 'Escape') this.hide(); |
|
|
}; |
|
|
document.addEventListener('keydown', this.escapeHandler); |
|
|
} |
|
|
|
|
|
|
|
|
document.body.appendChild(this.backdrop); |
|
|
document.body.appendChild(this.element); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
this.backdrop.classList.add('show'); |
|
|
this.element.classList.add('show'); |
|
|
}, 10); |
|
|
|
|
|
|
|
|
document.body.style.overflow = 'hidden'; |
|
|
|
|
|
|
|
|
this.trapFocus(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hide() { |
|
|
if (!this.element) return; |
|
|
|
|
|
|
|
|
this.backdrop.classList.remove('show'); |
|
|
this.element.classList.remove('show'); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
if (this.backdrop && this.backdrop.parentNode) { |
|
|
this.backdrop.parentNode.removeChild(this.backdrop); |
|
|
} |
|
|
if (this.element && this.element.parentNode) { |
|
|
this.element.parentNode.removeChild(this.element); |
|
|
} |
|
|
this.backdrop = null; |
|
|
this.element = null; |
|
|
|
|
|
|
|
|
document.body.style.overflow = ''; |
|
|
|
|
|
|
|
|
if (this.escapeHandler) { |
|
|
document.removeEventListener('keydown', this.escapeHandler); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.onClose) { |
|
|
this.onClose(); |
|
|
} |
|
|
}, 300); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setContent(html) { |
|
|
if (!this.element) return; |
|
|
const body = this.element.querySelector('.modal-body'); |
|
|
if (body) { |
|
|
body.innerHTML = html; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trapFocus() { |
|
|
const focusable = this.element.querySelectorAll( |
|
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' |
|
|
); |
|
|
|
|
|
if (focusable.length === 0) return; |
|
|
|
|
|
const firstFocusable = focusable[0]; |
|
|
const lastFocusable = focusable[focusable.length - 1]; |
|
|
|
|
|
firstFocusable.focus(); |
|
|
|
|
|
this.element.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Tab') { |
|
|
if (e.shiftKey && document.activeElement === firstFocusable) { |
|
|
lastFocusable.focus(); |
|
|
e.preventDefault(); |
|
|
} else if (!e.shiftKey && document.activeElement === lastFocusable) { |
|
|
firstFocusable.focus(); |
|
|
e.preventDefault(); |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
escapeHtml(text) { |
|
|
const div = document.createElement('div'); |
|
|
div.textContent = text; |
|
|
return div.innerHTML; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static confirm(message, onConfirm, onCancel) { |
|
|
const modal = new Modal({ |
|
|
title: 'Confirm', |
|
|
content: ` |
|
|
<p>${message}</p> |
|
|
<div class="modal-actions"> |
|
|
<button class="btn btn-secondary" id="modal-cancel">Cancel</button> |
|
|
<button class="btn btn-primary" id="modal-confirm">Confirm</button> |
|
|
</div> |
|
|
`, |
|
|
size: 'small', |
|
|
}); |
|
|
|
|
|
modal.show(); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
const confirmBtn = document.getElementById('modal-confirm'); |
|
|
const cancelBtn = document.getElementById('modal-cancel'); |
|
|
|
|
|
if (confirmBtn) { |
|
|
confirmBtn.addEventListener('click', () => { |
|
|
modal.hide(); |
|
|
if (onConfirm) onConfirm(); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (cancelBtn) { |
|
|
cancelBtn.addEventListener('click', () => { |
|
|
modal.hide(); |
|
|
if (onCancel) onCancel(); |
|
|
}); |
|
|
} |
|
|
}, 50); |
|
|
|
|
|
return modal; |
|
|
} |
|
|
} |
|
|
|
|
|
export default Modal; |
|
|
|