UNPKG

muspe-cli

Version:

MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo

539 lines (455 loc) 12.6 kB
// MusPE UI - Modal Component class Modal { constructor(options = {}) { this.options = { title: '', content: '', size: 'md', // sm, md, lg, xl, full closable: true, closeOnBackdrop: true, closeOnEscape: true, showHeader: true, showFooter: false, footerActions: [], animation: 'slideUp', // slideUp, slideDown, fade, zoom backdrop: true, ...options }; this.element = null; this.backdrop = null; this.isOpen = false; this.callbacks = { onOpen: null, onClose: null, onBackdropClick: null, ...options.callbacks }; } render() { // Create backdrop this.backdrop = document.createElement('div'); this.backdrop.className = 'muspe-modal-backdrop'; // Create modal this.element = document.createElement('div'); this.element.className = this.buildClasses(); this.element.innerHTML = ` <div class="muspe-modal__dialog"> ${this.options.showHeader ? this.renderHeader() : ''} <div class="muspe-modal__body"> ${this.renderContent()} </div> ${this.options.showFooter ? this.renderFooter() : ''} </div> `; this.attachEvents(); return this.element; } buildClasses() { const classes = ['muspe-modal']; classes.push(`muspe-modal--${this.options.size}`); classes.push(`muspe-modal--${this.options.animation}`); if (!this.options.backdrop) { classes.push('muspe-modal--no-backdrop'); } return classes.join(' '); } renderHeader() { return ` <div class="muspe-modal__header"> <h3 class="muspe-modal__title">${this.options.title}</h3> ${this.options.closable ? ` <button class="muspe-modal__close" type="button" aria-label="Close"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none"> <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button> ` : ''} </div> `; } renderContent() { if (typeof this.options.content === 'string') { return this.options.content; } else if (this.options.content instanceof HTMLElement) { return this.options.content.outerHTML; } else if (this.options.content && typeof this.options.content.render === 'function') { return this.options.content.render().outerHTML; } return ''; } renderFooter() { if (this.options.footerActions.length === 0) return ''; const actions = this.options.footerActions.map((action, index) => { const variant = action.variant || 'secondary'; return ` <button class="muspe-button muspe-button--${variant}" data-action="${index}"> ${action.text} </button> `; }).join(''); return ` <div class="muspe-modal__footer"> ${actions} </div> `; } attachEvents() { if (!this.element) return; // Close button const closeBtn = this.element.querySelector('.muspe-modal__close'); if (closeBtn) { closeBtn.addEventListener('click', () => this.close()); } // Footer actions const footerActions = this.element.querySelectorAll('[data-action]'); footerActions.forEach((btn, index) => { btn.addEventListener('click', () => { const action = this.options.footerActions[index]; if (action.callback) { action.callback(); } if (action.close !== false) { this.close(); } }); }); // Backdrop click if (this.backdrop) { this.backdrop.addEventListener('click', (e) => { if (e.target === this.backdrop && this.options.closeOnBackdrop) { if (this.callbacks.onBackdropClick) { this.callbacks.onBackdropClick(); } this.close(); } }); } // Escape key if (this.options.closeOnEscape) { this.escapeHandler = (e) => { if (e.key === 'Escape' && this.isOpen) { this.close(); } }; document.addEventListener('keydown', this.escapeHandler); } // Prevent dialog click from closing modal const dialog = this.element.querySelector('.muspe-modal__dialog'); if (dialog) { dialog.addEventListener('click', (e) => { e.stopPropagation(); }); } } open() { if (this.isOpen) return; // Add to DOM if (this.backdrop) { document.body.appendChild(this.backdrop); } document.body.appendChild(this.element); // Prevent body scroll document.body.style.overflow = 'hidden'; // Force reflow before adding classes this.element.offsetHeight; // Add open classes if (this.backdrop) { this.backdrop.classList.add('muspe-modal-backdrop--open'); } this.element.classList.add('muspe-modal--open'); this.isOpen = true; // Focus management this.trapFocus(); if (this.callbacks.onOpen) { this.callbacks.onOpen(); } } close() { if (!this.isOpen) return; // Remove open classes if (this.backdrop) { this.backdrop.classList.remove('muspe-modal-backdrop--open'); } this.element.classList.remove('muspe-modal--open'); // Wait for animation to complete setTimeout(() => { // Remove from DOM if (this.backdrop && this.backdrop.parentNode) { this.backdrop.parentNode.removeChild(this.backdrop); } if (this.element && this.element.parentNode) { this.element.parentNode.removeChild(this.element); } // Restore body scroll document.body.style.overflow = ''; this.isOpen = false; if (this.callbacks.onClose) { this.callbacks.onClose(); } }, 300); // Match CSS transition duration } trapFocus() { const focusableElements = this.element.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (focusableElements.length === 0) return; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; firstElement.focus(); this.focusHandler = (e) => { if (e.key === 'Tab') { if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } } }; document.addEventListener('keydown', this.focusHandler); } setContent(content) { this.options.content = content; const body = this.element?.querySelector('.muspe-modal__body'); if (body) { if (typeof content === 'string') { body.innerHTML = content; } else if (content instanceof HTMLElement) { body.innerHTML = ''; body.appendChild(content); } } } setTitle(title) { this.options.title = title; const titleElement = this.element?.querySelector('.muspe-modal__title'); if (titleElement) { titleElement.textContent = title; } } destroy() { // Remove event listeners if (this.escapeHandler) { document.removeEventListener('keydown', this.escapeHandler); } if (this.focusHandler) { document.removeEventListener('keydown', this.focusHandler); } // Close and remove from DOM if (this.isOpen) { this.close(); } this.element = null; this.backdrop = null; } } // CSS Styles for Modal const modalStyles = ` .muspe-modal-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); z-index: var(--muspe-z-modal); opacity: 0; transition: opacity var(--muspe-transition-normal); } .muspe-modal-backdrop--open { opacity: 1; } .muspe-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; z-index: calc(var(--muspe-z-modal) + 1); padding: var(--muspe-space-4); opacity: 0; transform: translateY(50px) scale(0.9); transition: all var(--muspe-transition-normal); } .muspe-modal--open { opacity: 1; transform: translateY(0) scale(1); } .muspe-modal__dialog { background: var(--muspe-white); border-radius: var(--muspe-radius-xl); box-shadow: var(--muspe-shadow-xl); max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; width: 100%; } /* Sizes */ .muspe-modal--sm .muspe-modal__dialog { max-width: 400px; } .muspe-modal--md .muspe-modal__dialog { max-width: 500px; } .muspe-modal--lg .muspe-modal__dialog { max-width: 700px; } .muspe-modal--xl .muspe-modal__dialog { max-width: 900px; } .muspe-modal--full .muspe-modal__dialog { max-width: none; width: 100%; height: 100%; max-height: 100vh; border-radius: 0; } /* Animations */ .muspe-modal--slideDown { transform: translateY(-50px) scale(0.9); } .muspe-modal--slideDown.muspe-modal--open { transform: translateY(0) scale(1); } .muspe-modal--fade { transform: scale(1); } .muspe-modal--fade.muspe-modal--open { transform: scale(1); } .muspe-modal--zoom { transform: scale(0.7); } .muspe-modal--zoom.muspe-modal--open { transform: scale(1); } /* Header */ .muspe-modal__header { display: flex; align-items: center; justify-content: space-between; padding: var(--muspe-space-6) var(--muspe-space-6) var(--muspe-space-4); border-bottom: 1px solid var(--muspe-gray-200); } .muspe-modal__title { font-size: var(--muspe-font-size-xl); font-weight: 600; color: var(--muspe-gray-900); margin: 0; } .muspe-modal__close { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: transparent; border: none; border-radius: var(--muspe-radius-md); color: var(--muspe-gray-500); cursor: pointer; transition: all var(--muspe-transition-fast); } .muspe-modal__close:hover { background: var(--muspe-gray-100); color: var(--muspe-gray-700); } /* Body */ .muspe-modal__body { flex: 1; padding: var(--muspe-space-6); overflow-y: auto; color: var(--muspe-gray-700); line-height: 1.6; } /* Footer */ .muspe-modal__footer { display: flex; gap: var(--muspe-space-3); justify-content: flex-end; padding: var(--muspe-space-4) var(--muspe-space-6) var(--muspe-space-6); border-top: 1px solid var(--muspe-gray-200); } /* Mobile optimizations */ @media (max-width: 640px) { .muspe-modal { padding: var(--muspe-space-2); align-items: flex-end; } .muspe-modal__dialog { max-height: 95vh; border-radius: var(--muspe-radius-xl) var(--muspe-radius-xl) 0 0; } .muspe-modal--full .muspe-modal__dialog { border-radius: 0; max-height: 100vh; } .muspe-modal__header, .muspe-modal__body, .muspe-modal__footer { padding-left: var(--muspe-space-4); padding-right: var(--muspe-space-4); } .muspe-modal__footer { flex-direction: column; } .muspe-modal__footer .muspe-button { width: 100%; } } /* Dark mode */ @media (prefers-color-scheme: dark) { .muspe-modal__dialog { background: var(--muspe-gray-800); } .muspe-modal__header { border-bottom-color: var(--muspe-gray-700); } .muspe-modal__title { color: var(--muspe-gray-100); } .muspe-modal__body { color: var(--muspe-gray-300); } .muspe-modal__footer { border-top-color: var(--muspe-gray-700); } .muspe-modal__close:hover { background: var(--muspe-gray-700); color: var(--muspe-gray-300); } } /* Safe area support */ @supports (padding: max(0px)) { .muspe-modal { padding-top: max(var(--muspe-space-4), env(safe-area-inset-top)); padding-bottom: max(var(--muspe-space-4), env(safe-area-inset-bottom)); padding-left: max(var(--muspe-space-4), env(safe-area-inset-left)); padding-right: max(var(--muspe-space-4), env(safe-area-inset-right)); } } /* Reduced motion */ @media (prefers-reduced-motion: reduce) { .muspe-modal-backdrop, .muspe-modal { transition: none; } } `; // Auto-inject styles if (typeof document !== 'undefined') { const styleSheet = document.createElement('style'); styleSheet.textContent = modalStyles; document.head.appendChild(styleSheet); } export default Modal;