397 lines
14 KiB
JavaScript
397 lines
14 KiB
JavaScript
// 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: `<svg class="w-6 h-6 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
|
</svg>`,
|
|
confirmButtonClass: 'bg-yellow-glass'
|
|
},
|
|
danger: {
|
|
title: 'Danger',
|
|
icon: `<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
|
</svg>`,
|
|
confirmButtonClass: 'bg-red-glass'
|
|
},
|
|
info: {
|
|
title: 'Information',
|
|
icon: `<svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>`,
|
|
confirmButtonClass: 'bg-blue-glass'
|
|
},
|
|
success: {
|
|
title: 'Success',
|
|
icon: `<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>`,
|
|
confirmButtonClass: 'bg-green-glass'
|
|
},
|
|
default: {
|
|
title: 'Confirm Action',
|
|
icon: `<svg class="w-6 h-6 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>`
|
|
}
|
|
},
|
|
|
|
// 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 = `
|
|
<div class="modal-content theme-glass rounded-lg shadow-xl border-0 outline-none ${this.defaults.width} mx-auto" style="background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(40px) saturate(200%); -webkit-backdrop-filter: blur(40px) saturate(200%); border: 1px solid var(--border-color-strong); box-shadow: var(--shadow-xl), inset 0 1px 0 rgba(255, 255, 255, 0.15); position: relative; overflow: hidden;">
|
|
<div style="position: absolute; top: 0; left: 0; right: 0; height: 40%; background: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0) 100%); pointer-events: none; border-radius: inherit; z-index: 0;"></div>
|
|
<div style="position: relative; z-index: 1;">
|
|
<div class="modal-header p-6 pb-4" style="border-bottom: 1px solid var(--border-color);">
|
|
<div class="flex items-center gap-3">
|
|
<div class="modal-icon flex-shrink-0"></div>
|
|
<h3 class="modal-title text-xl font-bold flex-1" style="color: var(--text-primary);"></h3>
|
|
</div>
|
|
</div>
|
|
<div class="modal-body p-6">
|
|
<div class="modal-message" style="color: var(--text-secondary);"></div>
|
|
</div>
|
|
<div class="modal-footer p-6 pt-4 flex justify-end gap-3" style="border-top: 1px solid var(--border-color);">
|
|
<button data-cancel-btn class="px-4 py-2 rounded-lg font-medium transition-colors focus:outline-none"></button>
|
|
<button data-confirm-btn class="px-4 py-2 rounded-lg font-medium transition-colors focus:outline-none"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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
|
|
});
|
|
};
|
|
|