admin: keep the copy confirmation in front of the access key modal (#10856)

* admin: raise nested modals above the ones already open

Bootstrap gives every modal and every backdrop the same z-index, so a
modal opened while another is showing paints behind it and its buttons
cannot be clicked. Viewing an access key secret and then copying a field
left the confirmation stuck behind the details modal with no way to
dismiss it.

Give each nested modal, and the backdrop Bootstrap creates for it, a
z-index above what is already on screen, and put back the scroll lock
that Bootstrap drops as soon as any one of them closes.

* admin: confirm clipboard copies with a toast

The access key details modal offers three copy buttons, and each one
raised a modal that had to be dismissed before the next copy. Confirm
with a toast instead, so the credentials stay in view and nothing has to
be clicked away.
This commit is contained in:
Chris Lu
2026-08-21 09:20:27 -07:00
committed by GitHub
parent 6c7f184381
commit bd34565e56
2 changed files with 69 additions and 5 deletions
+5 -5
View File
@@ -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');
});
+64
View File
@@ -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 = '<div class="d-flex"><div class="toast-body">' + escapeHtml(message) + '</div>' +
'<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button></div>';
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);