Files
datacenter-designer/public/js/lib/ui.js
Stefano Manfredi 3431a121a9 First commit
2025-10-27 11:57:38 +00:00

452 lines
11 KiB
JavaScript

/**
* UI Utilities
* Reusable UI components and helpers
*/
import CONFIG from '../config.js';
/**
* Modal Manager
* Simplifies modal open/close operations
*/
export class ModalManager {
/**
* Show a modal
* @param {string} modalId - ID of modal element
* @param {Function} onOpen - Optional callback when modal opens
*/
static show(modalId, onOpen = null) {
const modal = document.getElementById(modalId);
if (!modal) {
console.error(`Modal #${modalId} not found`);
return;
}
modal.classList.remove('hidden');
if (onOpen) {
onOpen(modal);
}
}
/**
* Hide a modal
* @param {string} modalId - ID of modal element
* @param {Function} onClose - Optional callback when modal closes
*/
static hide(modalId, onClose = null) {
const modal = document.getElementById(modalId);
if (!modal) {
console.error(`Modal #${modalId} not found`);
return;
}
modal.classList.add('hidden');
if (onClose) {
onClose(modal);
}
}
/**
* Setup modal with close handlers
* @param {string} modalId - ID of modal element
* @param {string} closeButtonId - ID of close button
* @param {Function} onClose - Optional cleanup callback
* @returns {Function} Cleanup function
*/
static setup(modalId, closeButtonId, onClose = null) {
const modal = document.getElementById(modalId);
const closeBtn = document.getElementById(closeButtonId);
if (!modal || !closeBtn) {
console.error(`Modal #${modalId} or close button #${closeButtonId} not found`);
return () => {};
}
const handleClose = () => {
this.hide(modalId, onClose);
};
// Close on button click
closeBtn.addEventListener('click', handleClose);
// Close on outside click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
handleClose();
}
});
// Close on ESC key
const handleEscape = (e) => {
if (e.key === 'Escape') {
handleClose();
}
};
document.addEventListener('keydown', handleEscape);
// Return cleanup function
return () => {
closeBtn.removeEventListener('click', handleClose);
document.removeEventListener('keydown', handleEscape);
};
}
}
/**
* Toast Notification System
* Better than window.alert()
*/
export class Toast {
static container = null;
/**
* Initialize toast container
*/
static init() {
if (this.container) return;
this.container = document.createElement('div');
this.container.id = 'toast-container';
this.container.style.cssText = `
position: fixed;
top: ${CONFIG.UI.TOOLBAR_HEIGHT + 20}px;
right: 20px;
z-index: ${CONFIG.COLORS.PRIMARY};
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
`;
document.body.appendChild(this.container);
}
/**
* Show a toast notification
* @param {string} message - Message to display
* @param {string} type - 'success', 'error', 'warning', 'info'
* @param {number} duration - Display duration in ms (0 = persist)
*/
static show(message, type = 'info', duration = 3000) {
this.init();
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const colors = {
success: CONFIG.COLORS.SUCCESS,
error: CONFIG.COLORS.DANGER,
warning: CONFIG.COLORS.WARNING,
info: CONFIG.COLORS.PRIMARY
};
toast.style.cssText = `
background-color: ${colors[type] || colors.info};
color: white;
padding: 12px 20px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-width: 400px;
word-wrap: break-word;
pointer-events: auto;
cursor: pointer;
animation: slideIn 0.3s ease;
font-size: 14px;
`;
toast.textContent = message;
// Click to dismiss
toast.addEventListener('click', () => {
this.remove(toast);
});
this.container.appendChild(toast);
// Auto-remove after duration
if (duration > 0) {
setTimeout(() => {
this.remove(toast);
}, duration);
}
return toast;
}
/**
* Remove a toast
*/
static remove(toast) {
toast.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}
// Convenience methods
static success(message, duration) {
return this.show(message, 'success', duration);
}
static error(message, duration) {
return this.show(message, 'error', duration);
}
static warning(message, duration) {
return this.show(message, 'warning', duration);
}
static info(message, duration) {
return this.show(message, 'info', duration);
}
}
/**
* Loading Indicator
*/
export class LoadingIndicator {
static overlay = null;
static show(message = 'Loading...') {
if (this.overlay) return;
this.overlay = document.createElement('div');
this.overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
`;
const spinner = document.createElement('div');
spinner.style.cssText = `
background-color: white;
padding: 30px 40px;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
text-align: center;
font-size: 16px;
color: #333;
`;
spinner.innerHTML = `
<div style="margin-bottom: 15px;">
<div class="spinner"></div>
</div>
${message}
`;
this.overlay.appendChild(spinner);
document.body.appendChild(this.overlay);
}
static hide() {
if (this.overlay && this.overlay.parentNode) {
this.overlay.parentNode.removeChild(this.overlay);
this.overlay = null;
}
}
}
/**
* Confirmation Dialog
* Better than window.confirm()
*/
export class Confirm {
/**
* Show confirmation dialog
* @param {string} message - Confirmation message
* @param {Object} options - Options {title, confirmText, cancelText, onConfirm, onCancel}
* @returns {Promise<boolean>} Resolves to true if confirmed
*/
static async show(message, options = {}) {
const {
title = 'Confirm',
confirmText = 'Confirm',
cancelText = 'Cancel',
danger = false
} = options;
return new Promise((resolve) => {
// Create modal
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h3>${title}</h3>
</div>
<div class="modal-body">
<p style="margin: 0; font-size: 14px; color: #666;">${message}</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary cancel-btn">${cancelText}</button>
<button class="btn ${danger ? 'btn-danger' : 'btn-primary'} confirm-btn">${confirmText}</button>
</div>
</div>
`;
document.body.appendChild(modal);
const confirmBtn = modal.querySelector('.confirm-btn');
const cancelBtn = modal.querySelector('.cancel-btn');
const cleanup = () => {
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
};
confirmBtn.addEventListener('click', () => {
cleanup();
resolve(true);
});
cancelBtn.addEventListener('click', () => {
cleanup();
resolve(false);
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
cleanup();
resolve(false);
}
});
});
}
}
/**
* Prompt Dialog
* Better than window.prompt()
*/
export class Prompt {
/**
* Show prompt dialog
* @param {string} message - Prompt message
* @param {Object} options - Options {title, defaultValue, placeholder, okText, cancelText}
* @returns {Promise<string|null>} Resolves to input value or null if cancelled
*/
static async show(message, options = {}) {
const {
title = 'Input Required',
defaultValue = '',
placeholder = '',
okText = 'OK',
cancelText = 'Cancel'
} = options;
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h3>${title}</h3>
</div>
<div class="modal-body">
<label style="display: block; margin-bottom: 8px; font-size: 14px; color: #666;">${message}</label>
<input type="text" class="form-input prompt-input" value="${defaultValue}" placeholder="${placeholder}" />
</div>
<div class="modal-footer">
<button class="btn btn-secondary cancel-btn">${cancelText}</button>
<button class="btn btn-primary ok-btn">${okText}</button>
</div>
</div>
`;
document.body.appendChild(modal);
const input = modal.querySelector('.prompt-input');
const okBtn = modal.querySelector('.ok-btn');
const cancelBtn = modal.querySelector('.cancel-btn');
input.focus();
input.select();
const cleanup = () => {
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
};
const submit = () => {
const value = input.value.trim();
cleanup();
resolve(value || null);
};
okBtn.addEventListener('click', submit);
cancelBtn.addEventListener('click', () => {
cleanup();
resolve(null);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
submit();
} else if (e.key === 'Escape') {
cleanup();
resolve(null);
}
});
});
}
}
/**
* Add CSS animations for toasts
*/
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid ${CONFIG.COLORS.PRIMARY};
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
// Initialize toast system
Toast.init();