diff --git a/weed/admin/static/js/admin.js b/weed/admin/static/js/admin.js index 175a424c5..b09790059 100644 --- a/weed/admin/static/js/admin.js +++ b/weed/admin/static/js/admin.js @@ -634,7 +634,7 @@ function formatDate(date) { function adminCopyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => { - showAlert('Copied to clipboard!', 'success'); + showToast('Copied to clipboard!'); }).catch(err => { console.error('Failed to copy text: ', err); fallbackCopyText(text); @@ -660,7 +660,7 @@ function fallbackCopyText(text) { try { const successful = document.execCommand('copy'); if (successful) { - showAlert('Copied to clipboard!', 'success'); + showToast('Copied to clipboard!'); } else { showAlert('Failed to copy to clipboard', 'danger'); } @@ -2265,11 +2265,11 @@ function copyFromInput(inputId) { try { const successful = document.execCommand('copy'); if (successful) { - showAlert('Copied to clipboard!', 'success'); + showToast('Copied to clipboard!'); } else { // Try modern clipboard API as fallback navigator.clipboard.writeText(input.value).then(() => { - showAlert('Copied to clipboard!', 'success'); + showToast('Copied to clipboard!'); }).catch(() => { showAlert('Failed to copy', 'danger'); }); @@ -2277,7 +2277,7 @@ function copyFromInput(inputId) { } catch (err) { // Try modern clipboard API as fallback navigator.clipboard.writeText(input.value).then(() => { - showAlert('Copied to clipboard!', 'success'); + showToast('Copied to clipboard!'); }).catch(() => { showAlert('Failed to copy', 'danger'); }); diff --git a/weed/admin/static/js/modal-alerts.js b/weed/admin/static/js/modal-alerts.js index e8ccd54b8..765561254 100644 --- a/weed/admin/static/js/modal-alerts.js +++ b/weed/admin/static/js/modal-alerts.js @@ -168,6 +168,36 @@ 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 = '
' + escapeHtml(message) + '
' + + '
'; + container.appendChild(toast); + + toast.addEventListener('hidden.bs.toast', function () { + toast.remove(); + }); + + new bootstrap.Toast(toast, { delay: 3000 }).show(); + }; + /** * Show a confirmation dialog using Bootstrap modal * @param {string} message - The confirmation message @@ -290,6 +320,40 @@ 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);