diff --git a/js/ajax_modal.js b/js/ajax_modal.js index 81a9824f..af12db20 100644 --- a/js/ajax_modal.js +++ b/js/ajax_modal.js @@ -1,61 +1,114 @@ -// Ajax Modal Load Script -$(document).on('click', '.ajax-modal', function (e) { - e.preventDefault(); - - const $trigger = $(this); - - // Prefer data-modal-url, fallback to href - let modalUrl = $trigger.data('modal-url') || $trigger.attr('href') || '#'; - const modalSize = $trigger.data('modal-size') || 'md'; - const modalId = 'ajaxModal_' + Date.now(); - - // If no usable URL, bail - if (!modalUrl || modalUrl === '#') { - console.warn('ajax-modal: No modal URL found on trigger:', this); - return; - } - - // Show loading spinner while fetching content - const loadingSpinner = ` - `; - $('.content-wrapper').append(loadingSpinner); - - // Make AJAX request - $.ajax({ - url: modalUrl, - method: 'GET', - dataType: 'json', - success: function (response) { - $('#modal-loading-spinner').remove(); - - if (response.error) { - alert(response.error); - return; - } - - const modalHtml = ` - `; - - $('.content-wrapper').append(modalHtml); - const $modal = $('#' + modalId); - $modal.modal('show'); - - $modal.on('hidden.bs.modal', function () { - $(this).remove(); - }); - }, - error: function (xhr, status, error) { - $('#modal-loading-spinner').remove(); - alert('Error loading modal content. Please try again.'); - console.error('Modal AJAX Error:', status, error); +// Ajax Modal Load Script (deduped + locked) +function hashKey(str) { + let h = 0; + for (let i = 0; i < str.length; i++) { + h = ((h << 5) - h + str.charCodeAt(i)) | 0; } - }); + return Math.abs(h).toString(36); +} + +$(document).on('click', '.ajax-modal', function (e) { + e.preventDefault(); + + const $trigger = $(this); + + // prevent spam clicks on same trigger + if ($trigger.data('ajaxModalLoading')) { + return; + } + + $trigger + .data('ajaxModalLoading', true) + .prop('disabled', true) + .addClass('disabled'); + + // Prefer data-modal-url, fallback to href + const modalUrl = $trigger.data('modal-url') || $trigger.attr('href') || '#'; + const modalSize = $trigger.data('modal-size') || 'md'; + + if (!modalUrl || modalUrl === '#') { + console.warn('ajax-modal: No modal URL found on trigger:', this); + + $trigger + .data('ajaxModalLoading', false) + .prop('disabled', false) + .removeClass('disabled'); + + return; + } + + // stable IDs based on URL (prevents duplicates) + const key = hashKey(String(modalUrl)); + const modalId = 'ajaxModal_' + key; + const spinnerId = 'modal-loading-spinner-' + key; + + // if modal already exists, just show it + const $existing = $('#' + modalId); + if ($existing.length) { + $existing.modal('show'); + + $trigger + .data('ajaxModalLoading', false) + .prop('disabled', false) + .removeClass('disabled'); + + return; + } + + // Show loading spinner while fetching content (deduped) + $('#' + spinnerId).remove(); + $('.content-wrapper').append(` +
+ +
+ `); + + $.ajax({ + url: modalUrl, + method: 'GET', + dataType: 'json' + }) + .done(function (response) { + $('#' + spinnerId).remove(); + + if (response && response.error) { + alert(response.error); + return; + } + + // guard against race: if another request already created it + if ($('#' + modalId).length) { + $('#' + modalId).modal('show'); + return; + } + + const modalHtml = ` + `; + + $('.content-wrapper').append(modalHtml); + + const $modal = $('#' + modalId); + $modal.modal('show'); + + $modal.on('hidden.bs.modal', function () { + $(this).remove(); + }); + }) + .fail(function (xhr, status, error) { + $('#' + spinnerId).remove(); + alert('Error loading modal content. Please try again.'); + console.error('Modal AJAX Error:', status, error); + }) + .always(function () { + $trigger + .data('ajaxModalLoading', false) + .prop('disabled', false) + .removeClass('disabled'); + }); });