| |
| |
| |
|
|
| 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; |
|
|