diff --git a/assets/js/base.js b/assets/js/base.js index 5a4ce3b..8925325 100644 --- a/assets/js/base.js +++ b/assets/js/base.js @@ -241,6 +241,11 @@ trigger.focus(); } } + // Announce the close so whoever opened the modal can undo optimistic UI or + // clean up a dynamically-inserted overlay. A modal can be dismissed four + // ways — the ✕ button, a Cancel button, a backdrop click, and Escape — and + // the last two are handled globally here, so button-only listeners miss them. + el.dispatchEvent(new CustomEvent('lt:modalclose', { bubbles: true })); } function closeAllModals() { @@ -2774,6 +2779,9 @@ setTimeout(() => { if (modalEl && modalEl.parentNode) modalEl.remove(); }, 300); resolve(value); }; + // Any dismissal counts as "no comment given", including a backdrop click or + // Escape, which close the overlay through the global handlers above. + modalEl.addEventListener('lt:modalclose', () => finish(null)); modalEl.querySelector('[data-modal-close]').addEventListener('click', () => finish(null)); document.getElementById(modalId + '_cancel').addEventListener('click', () => finish(null)); document.getElementById(modalId + '_confirm').addEventListener('click', () => { diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index f0e64c9..6d38c2c 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -550,7 +550,7 @@ function bulkClose() { `; document.body.insertAdjacentHTML('beforeend', modalHtml); - lt.modal.open('bulkCloseModal'); + openModalWithDismiss('bulkCloseModal', closeBulkCloseModal); } function closeBulkCloseModal() { @@ -633,7 +633,7 @@ function showBulkAssignModal() { `; document.body.insertAdjacentHTML('beforeend', modalHtml); - lt.modal.open('bulkAssignModal'); + openModalWithDismiss('bulkAssignModal', closeBulkAssignModal); setTimeout(() => { const inp = document.getElementById('bulkAssignUserInput'); if (inp) inp.focus(); }, 120); lt.api.get('/api/get_users.php') @@ -731,7 +731,7 @@ function showBulkPriorityModal() { `; document.body.insertAdjacentHTML('beforeend', modalHtml); - lt.modal.open('bulkPriorityModal'); + openModalWithDismiss('bulkPriorityModal', closeBulkPriorityModal); } function closeBulkPriorityModal() { @@ -845,7 +845,7 @@ function showBulkStatusModal() { `; document.body.insertAdjacentHTML('beforeend', modalHtml); - lt.modal.open('bulkStatusModal'); + openModalWithDismiss('bulkStatusModal', closeBulkStatusModal); } function closeBulkStatusModal() { @@ -942,7 +942,7 @@ function showBulkDeleteModal() { `; document.body.insertAdjacentHTML('beforeend', modalHtml); - lt.modal.open('bulkDeleteModal'); + openModalWithDismiss('bulkDeleteModal', closeBulkDeleteModal); } function closeBulkDeleteModal() { @@ -1032,6 +1032,22 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul input.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleSubmit(); }); document.getElementById(`${modalId}_cancel`).addEventListener('click', () => cleanup(onCancel)); modal.querySelector('[data-modal-close]').addEventListener('click', () => cleanup(onCancel)); + // Backdrop click / Escape close the overlay via base.js's global handlers. + modal.addEventListener('lt:modalclose', () => cleanup(onCancel)); +} + +/** + * Open a dynamically-inserted modal and make sure it tears itself down however it + * is dismissed. base.js handles backdrop clicks and Escape globally, so wiring + * only the ✕/Cancel buttons leaves the overlay in the DOM — and the next open + * inserts a second element with the same id, which then shadows the live one. + */ +function openModalWithDismiss(modalId, onDismiss) { + lt.modal.open(modalId); + const el = document.getElementById(modalId); + // lt.modal.close() early-returns once .is-open is gone, so the close call + // inside onDismiss cannot re-enter this listener. + if (el) el.addEventListener('lt:modalclose', onDismiss); } // ======================================== @@ -1069,7 +1085,7 @@ function quickStatusChange(ticketId, currentStatus) { `; document.body.insertAdjacentHTML('beforeend', modalHtml); - lt.modal.open('quickStatusModal'); + openModalWithDismiss('quickStatusModal', closeQuickStatusModal); } function closeQuickStatusModal() { @@ -1136,7 +1152,7 @@ function quickAssign(ticketId) { `; document.body.insertAdjacentHTML('beforeend', modalHtml); - lt.modal.open('quickAssignModal'); + openModalWithDismiss('quickAssignModal', closeQuickAssignModal); lt.api.get('/api/get_users.php') .then(data => { diff --git a/assets/js/ticket.js b/assets/js/ticket.js index aa4a6ea..3495475 100644 --- a/assets/js/ticket.js +++ b/assets/js/ticket.js @@ -529,7 +529,19 @@ function updateTicketStatus() { `); const modal = document.getElementById(modalId); lt.modal.open(modalId); - const cleanup = (ok) => { lt.modal.close(modalId); setTimeout(() => modal.remove(), 300); if (!ok) statusSelect.selectedIndex = 0; }; + let settled = false; + const cleanup = (ok) => { + if (settled) return; // lt.modal.close() below re-enters via lt:modalclose + settled = true; + lt.modal.close(modalId); + setTimeout(() => modal.remove(), 300); + if (!ok) statusSelect.selectedIndex = 0; + }; + // Backdrop click and Escape close the overlay through base.js's global + // handlers. Without this the dropdown kept displaying the new status + // while the server was never called, so the ticket looked closed until + // a reload revealed it was still open. + modal.addEventListener('lt:modalclose', () => cleanup(false)); modal.querySelector('[data-modal-close]').addEventListener('click', () => cleanup(false)); document.getElementById(`${modalId}_cancel`).addEventListener('click', () => cleanup(false)); document.getElementById(`${modalId}_confirm`).addEventListener('click', () => {