@rxxuzi/gumi
Version:
Clean & minimal design system with delightful interactions
199 lines • 5.93 kB
JavaScript
// 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