// Highly customizable confirmation modal component // Supports custom titles, messages, buttons, icons, types, and more const ConfirmationModal = { // Default configuration defaults: { title: 'Confirm Action', message: 'Are you sure you want to proceed?', type: 'default', // 'default', 'warning', 'danger', 'info', 'success' confirmText: 'Confirm', cancelText: 'Cancel', confirmButtonClass: '', cancelButtonClass: '', showCancel: true, allowHTML: false, icon: null, // Custom icon HTML or null for default icons onConfirm: null, onCancel: null, closeOnBackdrop: true, closeOnEscape: true, focusConfirm: true, width: 'max-w-md', // Tailwind width class zIndex: 'z-50' }, // Type-specific configurations typeConfigs: { warning: { title: 'Warning', icon: ` `, confirmButtonClass: 'bg-yellow-glass' }, danger: { title: 'Danger', icon: ` `, confirmButtonClass: 'bg-red-glass' }, info: { title: 'Information', icon: ` `, confirmButtonClass: 'bg-blue-glass' }, success: { title: 'Success', icon: ` `, confirmButtonClass: 'bg-green-glass' }, default: { title: 'Confirm Action', icon: ` ` } }, // Create and show the modal show: function(options = {}) { return new Promise((resolve, reject) => { // Merge options with defaults and type config const config = { ...this.defaults, ...options }; const typeConfig = this.typeConfigs[config.type] || this.typeConfigs.default; // Apply type-specific config if (config.type !== 'default' && !options.title) { config.title = typeConfig.title; } if (!config.icon && typeConfig.icon) { config.icon = typeConfig.icon; } if (config.type !== 'default' && !options.confirmButtonClass) { config.confirmButtonClass = typeConfig.confirmButtonClass; } // Get or create modal element let modal = document.getElementById('confirmationModal'); if (!modal) { modal = this._createModalElement(); document.body.appendChild(modal); } // Update modal content this._updateModalContent(modal, config); // Set up event handlers const confirmBtn = modal.querySelector('[data-confirm-btn]'); const cancelBtn = modal.querySelector('[data-cancel-btn]'); const backdrop = modal.querySelector('.modal-backdrop'); // Clean up previous handlers const newConfirmHandler = () => { modal.close(); if (config.onConfirm) { try { const result = config.onConfirm(); if (result instanceof Promise) { result.then(resolve).catch(reject); } else { resolve(result); } } catch (error) { reject(error); } } else { resolve(true); } this._cleanup(modal); }; const newCancelHandler = () => { modal.close(); if (config.onCancel) { try { const result = config.onCancel(); if (result instanceof Promise) { result.then(() => resolve(false)).catch(reject); } else { resolve(false); } } catch (error) { reject(error); } } else { resolve(false); } this._cleanup(modal); }; const escapeHandler = (e) => { if (config.closeOnEscape && e.key === 'Escape') { e.preventDefault(); newCancelHandler(); } else if (e.key === 'Enter' && config.focusConfirm) { e.preventDefault(); newConfirmHandler(); } }; // Attach handlers confirmBtn.addEventListener('click', newConfirmHandler); if (cancelBtn) { cancelBtn.addEventListener('click', newCancelHandler); } document.addEventListener('keydown', escapeHandler); // Store handlers for cleanup modal._handlers = { confirm: newConfirmHandler, cancel: newCancelHandler, escape: escapeHandler }; // Backdrop click handler if (config.closeOnBackdrop) { const backdropHandler = (e) => { if (e.target === modal) { newCancelHandler(); } }; modal.addEventListener('click', backdropHandler); modal._handlers.backdrop = backdropHandler; } // Show modal modal.showModal(); // Focus confirm button if specified if (config.focusConfirm) { setTimeout(() => confirmBtn.focus(), 100); } else if (cancelBtn) { setTimeout(() => cancelBtn.focus(), 100); } }); }, // Create the modal DOM element _createModalElement: function() { const modal = document.createElement('dialog'); modal.id = 'confirmationModal'; modal.className = 'confirmation-modal p-0 bg-transparent border-0 outline-none rounded-lg shadow-2xl w-full max-w-md'; modal.setAttribute('style', 'border: none; outline: none; padding: 0; margin: 0; background: transparent;'); modal.innerHTML = ` `; return modal; }, // Update modal content based on config _updateModalContent: function(modal, config) { const titleEl = modal.querySelector('.modal-title'); const messageEl = modal.querySelector('.modal-message'); const iconEl = modal.querySelector('.modal-icon'); const confirmBtn = modal.querySelector('[data-confirm-btn]'); const cancelBtn = modal.querySelector('[data-cancel-btn]'); const footer = modal.querySelector('.modal-footer'); const content = modal.querySelector('.modal-content'); // Update title if (titleEl) { titleEl.textContent = config.title; } // Update message if (messageEl) { if (config.allowHTML) { messageEl.innerHTML = config.message; } else { messageEl.textContent = config.message; } } // Update icon if (iconEl) { if (config.icon) { iconEl.innerHTML = config.icon; iconEl.classList.remove('hidden'); } else { iconEl.classList.add('hidden'); } } // Update buttons with glass styling if (confirmBtn) { confirmBtn.textContent = config.confirmText; confirmBtn.className = 'px-4 py-2 rounded-lg font-medium transition-all focus:outline-none btn-glass-primary'; // Apply type-specific colors let bgColor, borderColor; if (config.type === 'warning') { bgColor = 'rgba(245, 158, 11, 0.3)'; borderColor = 'rgba(245, 158, 11, 0.5)'; } else if (config.type === 'danger') { bgColor = 'rgba(239, 68, 68, 0.3)'; borderColor = 'rgba(239, 68, 68, 0.5)'; } else if (config.type === 'info') { bgColor = 'rgba(59, 130, 246, 0.3)'; borderColor = 'rgba(59, 130, 246, 0.5)'; } else if (config.type === 'success') { bgColor = 'rgba(16, 185, 129, 0.3)'; borderColor = 'rgba(16, 185, 129, 0.5)'; } else { bgColor = 'rgba(99, 102, 241, 0.3)'; borderColor = 'rgba(99, 102, 241, 0.5)'; } confirmBtn.style.cssText = ` background: ${bgColor}; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); border: 1px solid ${borderColor}; color: var(--text-primary); box-shadow: 0 4px 6px -1px ${borderColor.replace('0.5', '0.2')}, 0 2px 4px -1px ${borderColor.replace('0.5', '0.1')}, inset 0 1px 0 rgba(255, 255, 255, 0.1); position: relative; overflow: hidden; `; confirmBtn.addEventListener('mouseenter', function() { this.style.background = bgColor.replace('0.3', '0.5'); this.style.borderColor = borderColor.replace('0.5', '0.7'); this.style.transform = 'translateY(-2px)'; }); confirmBtn.addEventListener('mouseleave', function() { this.style.background = bgColor; this.style.borderColor = borderColor; this.style.transform = 'translateY(0)'; }); } if (cancelBtn) { if (config.showCancel) { cancelBtn.textContent = config.cancelText; cancelBtn.className = 'px-4 py-2 rounded-lg font-medium transition-all focus:outline-none btn-glass'; cancelBtn.style.cssText = ` background: var(--bg-glass); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); border: 1px solid var(--border-color); color: var(--text-primary); box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.03); position: relative; overflow: hidden; `; cancelBtn.addEventListener('mouseenter', function() { this.style.background = 'var(--bg-glass-hover)'; this.style.borderColor = 'var(--border-color-strong)'; this.style.boxShadow = 'var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.08)'; }); cancelBtn.addEventListener('mouseleave', function() { this.style.background = 'var(--bg-glass)'; this.style.borderColor = 'var(--border-color)'; this.style.boxShadow = 'var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.03)'; }); cancelBtn.classList.remove('hidden'); } else { cancelBtn.classList.add('hidden'); } } // Update width if (content && config.width) { content.className = content.className.replace(/max-w-\w+/, ''); content.classList.add(config.width); } }, // Clean up event handlers _cleanup: function(modal) { if (!modal._handlers) return; const confirmBtn = modal.querySelector('[data-confirm-btn]'); const cancelBtn = modal.querySelector('[data-cancel-btn]'); if (confirmBtn && modal._handlers.confirm) { confirmBtn.removeEventListener('click', modal._handlers.confirm); } if (cancelBtn && modal._handlers.cancel) { cancelBtn.removeEventListener('click', modal._handlers.cancel); } if (modal._handlers.escape) { document.removeEventListener('keydown', modal._handlers.escape); } if (modal._handlers.backdrop) { modal.removeEventListener('click', modal._handlers.backdrop); } delete modal._handlers; }, // Convenience methods for common types warning: function(message, options = {}) { return this.show({ ...options, message, type: 'warning' }); }, danger: function(message, options = {}) { return this.show({ ...options, message, type: 'danger' }); }, info: function(message, options = {}) { return this.show({ ...options, message, type: 'info' }); }, success: function(message, options = {}) { return this.show({ ...options, message, type: 'success' }); }, // Simple confirm replacement (backward compatible) confirm: function(message, options = {}) { return this.show({ ...options, message }); }, // Alert-style modal (single button, no cancel) alert: function(message, options = {}) { return this.show({ ...options, message, showCancel: false, confirmText: options.confirmText || 'OK', type: options.type || 'info', focusConfirm: true }); } }; // Make it globally available window.ConfirmationModal = ConfirmationModal; // Also provide a simple showConfirm function for backward compatibility window.showConfirm = function(message, callback, options = {}) { return ConfirmationModal.show({ ...options, message, onConfirm: callback }); };