UNPKG

@rxxuzi/gumi

Version:

Clean & minimal design system with delightful interactions

199 lines 5.93 kB
// components/modal.ts // Gumi.js v1.0.0 - Modal Component import { $, $$, on, off, trigger } from '../core/dom'; import { animate } from '../core/animation'; import { generateId } from '../utils/helpers'; export class Modal { constructor(element, options = {}) { this.backdrop = null; this.isOpen = false; this.escapeHandler = null; this.keydownListener = null; const el = $(element); if (!el) throw new Error('Modal element not found'); this.element = el; this.options = { backdrop: true, keyboard: true, focus: true, ...options }; this.init(); } /** * Initialize modal */ init() { // Set initial styles this.element.style.display = 'none'; this.element.setAttribute('role', 'dialog'); this.element.setAttribute('aria-modal', 'true'); if (!this.element.id) { this.element.id = generateId('modal'); } } /** * Open modal */ open() { if (this.isOpen) return; this.isOpen = true; // Create backdrop if needed if (this.options.backdrop) { this.createBackdrop(); } // Style the modal Object.assign(this.element.style, { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%) scale(0.9)', zIndex: '1050', opacity: '0', maxWidth: '90vw', maxHeight: '90vh', overflow: 'auto', display: 'block' }); // Prevent body scroll document.body.style.overflow = 'hidden'; // Animate in if (this.backdrop) { animate(this.backdrop, [ { opacity: 0 }, { opacity: 1 } ], { duration: 200 }); } animate(this.element, [ { opacity: 0, transform: 'translate(-50%, -50%) scale(0.9)' }, { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' } ], { duration: 300 }); // Focus management if (this.options.focus) { this.element.focus(); } // Keyboard support if (this.options.keyboard) { this.escapeHandler = (e) => { if (e.key === 'Escape') { this.close(); } }; this.keydownListener = (e) => { if (this.escapeHandler) { this.escapeHandler(e); } }; on(document, 'keydown', this.keydownListener); } // Dispatch open event trigger(this.element, 'modal-open', { modal: this.element }); } /** * Close modal */ close() { if (!this.isOpen) return; this.isOpen = false; // Remove escape handler if (this.keydownListener) { off(document, 'keydown', this.keydownListener); this.keydownListener = null; this.escapeHandler = null; } // Animate out const animations = [ animate(this.element, [ { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' }, { opacity: 0, transform: 'translate(-50%, -50%) scale(0.9)' } ], { duration: 200 }) ]; if (this.backdrop) { animations.push(animate(this.backdrop, [ { opacity: 1 }, { opacity: 0 } ], { duration: 200 })); } Promise.all(animations).then(() => { this.element.style.display = 'none'; document.body.style.overflow = ''; if (this.backdrop) { this.backdrop.remove(); this.backdrop = null; } // Dispatch close event trigger(this.element, 'modal-close', { modal: this.element }); }); } /** * Toggle modal */ toggle() { if (this.isOpen) { this.close(); } else { this.open(); } } /** * Create backdrop */ createBackdrop() { // Remove any existing backdrop const existingBackdrop = $('.gumi-modal-backdrop'); if (existingBackdrop) { existingBackdrop.remove(); } this.backdrop = document.createElement('div'); this.backdrop.className = 'gumi-modal-backdrop'; Object.assign(this.backdrop.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', background: 'rgba(0, 0, 0, 0.5)', backdropFilter: 'blur(4px)', zIndex: '1040', opacity: '0' }); // Close on backdrop click on(this.backdrop, 'click', () => this.close()); document.body.appendChild(this.backdrop); } /** * Destroy modal instance */ destroy() { this.close(); this.element.removeAttribute('role'); this.element.removeAttribute('aria-modal'); } /** * Static method to initialize modals from triggers */ static initFromTriggers(selector = '[data-modal]') { const triggers = $$(selector); const modals = []; triggers.forEach(trigger => { const modalId = trigger.getAttribute('data-modal'); if (!modalId) return; const modalEl = $(modalId); if (!modalEl) return; const modal = new Modal(modalEl); modals.push(modal); on(trigger, 'click', (e) => { e.preventDefault(); modal.open(); }); }); return modals; } } //# sourceMappingURL=modal.js.map