routerconsole/jsp/js/toggleElements.js

/**
 * @module toggleElements
 * @description Generic toggle system for showing/hiding DOM elements via CSS selectors.
 * Supports single-section-at-a-time display mode, expand/collapse state classes,
 * and scroll-into-view on expand.
 * @author dr|z3d
 * @license AGPL3 or later
 */

/**
 * Sets up toggle behavior for elements matching the given selectors.
 * @function setupToggles
 * @param {string} toggleSelector - CSS selector for toggle trigger elements
 * @param {string} hiddenSelector - CSS selector for elements that should be toggled
 * @param {string} displayStyle - CSS display value when expanded (e.g., "block", "table-row-group")
 * @param {boolean} [collapseByDefault=true] - Whether to hide elements initially
 * @param {boolean} [displaySingle=true] - Whether only one section can be open at a time
 * @returns {void}
 */
function setupToggles(toggleSelector, hiddenSelector, displayStyle, collapseByDefault = true, displaySingle = true) {
  const toggleElements = document.querySelectorAll(toggleSelector);
  const hiddenElements = document.querySelectorAll(hiddenSelector);

  if (toggleElements && hiddenElements) {document.body.classList.add("toggleElementsActive");}
  else {return;}

  hiddenElements.forEach(function(hiddenElement) {
    if (!hiddenElement.classList.contains("config")) {
      if (collapseByDefault) {hiddenElement.style.display = "none";}
      else {hiddenElement.style.display = displayStyle;}
    }
  });

  toggleElements.forEach(function(toggleElement) {
    var hiddenElement = toggleElement.nextElementSibling;

    while (hiddenElement && hiddenElement.nodeType !== Node.ELEMENT_NODE) {
      hiddenElement = hiddenElement.nextElementSibling;
    }

    if (hiddenElement) {
      toggleElement.addEventListener("click", function() {
        if (displaySingle) {
          const relevantToggleElements = Array.from(toggleElements).filter(function(element) {
            return element.nextElementSibling === hiddenElement || element.nextElementSibling && element.nextElementSibling.classList.contains("toggle");
          });

          relevantToggleElements.forEach(function(otherToggleElement) {
            if (otherToggleElement !== toggleElement && otherToggleElement.classList.contains("expanded")) {
              const otherHiddenElement = otherToggleElement.nextElementSibling;
              if (otherHiddenElement.nodeType === Node.ELEMENT_NODE) {
                otherHiddenElement.style.display = "none";
                otherToggleElement.classList.remove("expanded");
              }
            }
          });

          const nonExpandedHiddenElements = Array.from(hiddenElements).filter(function(element) {
            return !element.classList.contains("expanded") && element !== hiddenElement;
          });

          nonExpandedHiddenElements.forEach(function(nonExpandedHiddenElement) {
            nonExpandedHiddenElement.style.display = "none";
          });

          toggleElements.forEach(function(otherToggleElement) {
            if (otherToggleElement !== toggleElement) {
              otherToggleElement.classList.remove("expanded");
            }
          });
        }

        if (!toggleElement.classList.contains("expanded")) {
          hiddenElement.style.display = displayStyle;
          toggleElement.classList.add("expanded");
          document.documentElement.classList.remove("hasCollapsedElement");
          toggleElement.scrollIntoView({block: "center", inline: "center"});
        } else {
          hiddenElement.style.display = "none";
          toggleElement.classList.remove("expanded");
          document.documentElement.classList.add("hasCollapsedElement");
        }
        if ([...toggleElements].some(element => element.classList.contains('expanded'))) {document.body.classList.add("hasExpandedElement");}
        else {document.body.classList.remove("hasExpandedElement");}
      });
      toggleElement.classList.add("toggle");
    }
  });
}