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
JavaScript
// 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 */
(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 */
(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 */
(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 */
(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;