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