Files
p2ns/includes/admin/admin-frontend/ui/confirmation-modal.js
T
2025-12-17 20:05:50 -05:00

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
});
};