/** * Modal Alerts - Bootstrap Modal replacement for native alert() and confirm() * Fixes Chrome auto-dismiss issue with native dialogs * * Usage: * showAlert('Message', 'success'); * showConfirm('Delete this?', function() { }); */ (function () { 'use strict'; // Create and inject modal HTML into page if not already present function ensureModalsExist() { if (document.getElementById('globalAlertModal')) { return; // Already exists } const modalsHTML = `
`; // Inject modals at end of body document.body.insertAdjacentHTML('beforeend', modalsHTML); } /** * Show an alert message using Bootstrap modal * @param {string} message - The message to display * @param {string|object} typeOrOptions - Type ('success', 'error', 'warning', 'info') or options object * @param {string} title - Optional custom title */ window.showAlert = function (message, typeOrOptions, title) { ensureModalsExist(); let type = 'info'; let isHtml = false; if (typeof typeOrOptions === 'object' && typeOrOptions !== null) { type = typeOrOptions.type || 'info'; isHtml = typeOrOptions.isHtml || false; title = typeOrOptions.title || title; } else if (typeof typeOrOptions === 'string') { type = typeOrOptions; } const modal = document.getElementById('globalAlertModal'); const header = document.getElementById('globalAlertModalHeader'); const titleEl = document.getElementById('globalAlertModalTitle'); const bodyEl = document.getElementById('globalAlertModalBody'); const iconEl = document.getElementById('globalAlertModalIcon'); // Configuration for different types const types = { 'success': { title: 'Success', icon: 'fa-check-circle', headerClass: 'bg-success text-white', btnClose: 'btn-close-white' }, 'error': { title: 'Error', icon: 'fa-exclamation-triangle', headerClass: 'bg-danger text-white', btnClose: 'btn-close-white' }, 'warning': { title: 'Warning', icon: 'fa-exclamation-circle', headerClass: 'bg-warning text-dark', btnClose: '' }, 'info': { title: 'Notice', icon: 'fa-info-circle', headerClass: 'bg-info text-white', btnClose: 'btn-close-white' } }; const config = types[type] || types['info']; // Update header styling header.className = 'modal-header ' + config.headerClass; const closeBtn = header.querySelector('.btn-close'); closeBtn.className = 'btn-close ' + config.btnClose; // Update icon iconEl.className = 'fas ' + config.icon + ' me-2'; // Update title titleEl.textContent = title || config.title; // Update body - support HTML or text if (isHtml || message.includes('') || message.includes('
' + escapeHtml(message) + '
'; } // Show modal const bsModal = new bootstrap.Modal(modal); bsModal.show(); }; /** * Show a transient message that does not have to be dismissed * @param {string} message - The message to display * @param {string} type - Bootstrap contextual color, defaults to 'success' */ window.showToast = function (message, type) { let container = document.getElementById('globalToastContainer'); if (!container) { container = document.createElement('div'); container.id = 'globalToastContainer'; container.className = 'toast-container position-fixed top-0 end-0 p-3'; // Above any modal, however deeply they are stacked. container.style.zIndex = '9999'; document.body.appendChild(container); } const toast = document.createElement('div'); toast.className = 'toast align-items-center text-white bg-' + (type || 'success') + ' border-0'; toast.setAttribute('role', 'alert'); toast.innerHTML = '') || message.includes('
' + escapeHtml(message) + '
'; } // Remove old event listeners by cloning buttons const newOkBtn = okBtn.cloneNode(true); const newCancelBtn = cancelBtn.cloneNode(true); okBtn.parentNode.replaceChild(newOkBtn, okBtn); cancelBtn.parentNode.replaceChild(newCancelBtn, cancelBtn); const modal = new bootstrap.Modal(modalEl); // Add event listeners newOkBtn.addEventListener('click', function () { modal.hide(); if (typeof onConfirm === 'function') { onConfirm(); } }); newCancelBtn.addEventListener('click', function () { modal.hide(); if (typeof onCancel === 'function') { onCancel(); } }); modal.show(); }; /** * Show a delete confirmation dialog * @param {string} itemName - Name of the item to delete * @param {function} onConfirm - Callback function if user confirms deletion * @param {string} message - Optional custom message (default: "Are you sure you want to delete this item?") */ window.showDeleteConfirm = function (itemName, onConfirm, message) { ensureModalsExist(); const modalEl = document.getElementById('globalDeleteModal'); const messageEl = document.getElementById('globalDeleteModalMessage'); const itemNameEl = document.getElementById('globalDeleteModalItemName'); const confirmBtn = document.getElementById('globalDeleteConfirmBtn'); // Set custom message if provided if (message) { messageEl.textContent = message; } else { messageEl.textContent = 'Are you sure you want to delete this item?'; } // Set item name itemNameEl.textContent = itemName; // Remove old event listener by cloning button const newConfirmBtn = confirmBtn.cloneNode(true); confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn); const modal = new bootstrap.Modal(modalEl); // Add new event listener newConfirmBtn.addEventListener('click', function () { modal.hide(); if (typeof onConfirm === 'function') { onConfirm(); } }); modal.show(); }; /** * Escape HTML to prevent XSS */ function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, function (m) { return map[m]; }); } // Bootstrap gives every modal and backdrop the same z-index, so a modal // opened while another one is showing paints behind it and cannot be // clicked. Lift each nested modal, and its backdrop, above what is already // on screen. const MODAL_Z_INDEX = 1055; const MODAL_Z_INDEX_STEP = 20; document.addEventListener('show.bs.modal', function (event) { const depth = document.querySelectorAll('.modal.show').length; if (depth === 0) { return; } const zIndex = MODAL_Z_INDEX + depth * MODAL_Z_INDEX_STEP; event.target.style.zIndex = zIndex; // Bootstrap only creates the backdrop after this event returns. setTimeout(function () { const backdrops = document.querySelectorAll('.modal-backdrop'); const backdrop = backdrops[backdrops.length - 1]; if (backdrop) { backdrop.style.zIndex = zIndex - 1; } }, 0); }); document.addEventListener('hidden.bs.modal', function (event) { event.target.style.zIndex = ''; // Closing any modal releases the scroll lock the others still need. if (document.querySelector('.modal.show')) { document.body.classList.add('modal-open'); } }); // Auto-initialize on DOMContentLoaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', ensureModalsExist); } else { ensureModalsExist(); } /** * AUTOMATIC OVERRIDE of native alert() * This makes ALL existing alert() calls automatically use Bootstrap modals */ window.alert = function (message) { // Auto-detect message type from content let type = 'info'; const msg = String(message || ''); const msgLower = msg.toLowerCase(); // Refined type inference to avoid false positives if (msgLower.includes('success') || msgLower.includes('successfully') || msgLower.includes('created') || msgLower.includes('updated') || msgLower.includes('saved')) { // Avoid "not successful" if (!msgLower.includes('not success')) { type = 'success'; } } if (type === 'info') { if (msgLower.includes('error') || msgLower.includes('failed') || msgLower.includes('invalid') || msgLower.includes('exception')) { type = 'error'; } else if (msgLower.includes('warning') || msgLower.includes('required') || msgLower.includes('attention')) { type = 'warning'; } } showAlert(msg, type); }; console.log('Modal Alerts library loaded - native alert() overridden'); })();