Ajax file upload throbber for a dynamic webform

CSS

span.ajax-throbber.icon {
  min-height: 0px;
  min-width: 0px;
  padding-left: 0px;
  position: relative;
}

JS

(function (jQuery, Drupal) {
  jQuery(document).ready(function () {

    // Store the observers for re-initializations.
    var observer = null;
    var globalObserver = null;

    // Enable/Disable function for the throbber and "Previous" / "Next" button(s).
    function toggleThrobber(show) {
      var $throbber = jQuery('.ajax-throbber');
      var $buttonNext = jQuery('button.webform-button--next');
      var $buttonPrev = jQuery('button.webform-button--previous');

      if (show) {
        $buttonNext.each(function () {
          jQuery(this).prop('disabled', true);
        });
        $buttonPrev.each(function () {
          jQuery(this).prop('disabled', true);
        });
        $throbber.each(function () {
          jQuery(this).show();
        });
      } else {
        $buttonNext.each(function () {
          jQuery(this).prop('disabled', false);
        });
        $buttonPrev.each(function () {
          jQuery(this).prop('disabled', false);
        });
        $throbber.each(function () {
          jQuery(this).hide();
        });
      }
    }

    // Add the throbber markup to the AJAX wrapper elements.
    function addThrobberToAjaxWrappers() {
      jQuery('div[id^="ajax-wrapper"]').each(function () {
        if (!jQuery('body').find('.ajax-throbber').length) {
          var $throbber = jQuery('<span class="ajax-throbber glyphicon-spin icon glyphicon glyphicon-refresh" aria-hidden="true"></span>');
          $throbber.hide();
          jQuery(this).append($throbber);
        }
      });
    }

    // Verify that the Attachment_upload_button is present.
    function waitForUploadButton() {
      var intervalId = setInterval(function () {
        if (jQuery('button[name="Attachment_upload_button"]').length) {
          clearInterval(intervalId);
          addThrobberToAjaxWrappers();
        }
      }, 100);

      setTimeout(function () {
        clearInterval(intervalId);
        console.warn('Le bouton "Attachment_upload_button" n\'a pas été trouvé après 300 secondes.');
      }, 300000); // 5 minutes max
    }

    // Observe the changes to detect uploaded files.
    function observeFileUploads() {
      // Nettoyer l'ancien observer s'il existe
      if (observer) {
        observer.disconnect();
      }
      if (globalObserver) {
        globalObserver.disconnect();
      }

      observer = new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {
          if (mutation.type === 'childList') {
            var hasFiles = jQuery('div.form-item.form-type-checkbox').find('input').length > 0;
            // toggleThrobber(!hasFiles);
          }
        });
      });

      jQuery('div[id^="ajax-wrapper--"]').each(function () {
        observer.observe(this, { childList: true, subtree: true });
      });

      // Observe the entire document to detect new `ajax-wrapper--`.
      globalObserver = new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {
          mutation.addedNodes.forEach(function (node) {
            if (node.nodeType === 1 && node.id && node.id.startsWith('ajax-wrapper--')) {
              toggleThrobber(false);
              observer.observe(node, { childList: true, subtree: true });
            }
          });
        });
      });

      globalObserver.observe(document.body, { childList: true, subtree: true });
    }

    // Listen to AJAX to start and stop the throbber.
    jQuery(document).ajaxSend(function (event, xhr, settings) {
      toggleThrobber(true);
    });

    jQuery(document).ajaxComplete(function (event, xhr, settings) {
      waitForUploadButton();
      addThrobberToAjaxWrappers();
      observeFileUploads(); // Réinitialiser le MutationObserver après chaque AJAX.
    });

    // Initialization.
    waitForUploadButton();
    addThrobberToAjaxWrappers();
    observeFileUploads();
  });
})(jQuery, Drupal);