routerconsole/jsp/js/sectionToggle.js

/**
 * @module sectionToggle
 * @description Provides sidebar section toggle functionality for the I2P+ web console.
 * Manages expand/collapse state persistence and the click listeners for the
 * sidebar toggles. State application and storage are delegated to the
 * sidebarState.js classic script (window.applySidebarState /
 * window.setSidebarSectionState / window.readSidebarState /
 * window.saveSidebarState), which runs as soon as the sidebar markup is parsed
 * so collapsed sections never flash expanded, with a cookie fallback when
 * localStorage is unavailable.
 * @author dr|z3d
 * @license AGPLv3 or later
 */

import { stickySidebar } from "/js/stickySidebar.js";

const sb = document.getElementById("sidebar");

/** @type {HTMLElement|null} */
let listenersTarget = null;
/** @type {EventListener|null} */
let listenersHandler = null;

/**
 * Initializes sidebar section toggle controls, restoring persisted states
 * and attaching click listeners for toggle interactions.
 * @function sectionToggler
 * @returns {void}
 */
function sectionToggler() {
  const sb_wrap = document.getElementById("sb_wrap") || sb;
  const savedConfigs = typeof window.readSidebarState === "function" ? window.readSidebarState() : null;
  let sidebarSections = savedConfigs !== null ? savedConfigs : null;

  /**
   * Initializes storage with default sidebar section visibility states.
   * @function initializeLocalStorage
   * @returns {Promise<Object>} Resolves with the default state object
   */
  function initializeLocalStorage() {
    return new Promise((resolve) => {
      const defaultState = {
        advancedgeneral: false,
        advanced: false,
        bandwidth: false,
        general: false,
        help: false,
        internals: false,
        localtunnels: false,
        newsheadings: false,
        peers: false,
        queue: false,
        services: false,
        tunnels: false,
        updatesection: false
      };
      if (typeof window.saveSidebarState === "function") {
        window.saveSidebarState(defaultState);
      }
      resolve(defaultState);
    });
  }

  /**
   * Saves the current toggle state of the active element to storage.
   * @async
   * @function saveToggleStates
   * @returns {Promise<void>}
   */
  async function saveToggleStates() {
    const toggleInput = document.activeElement;
    if (toggleInput && toggleInput.id.startsWith("toggle_sb_")) {
      const key = toggleInput.id.replace("toggle_sb_", "");
      if (!savedConfigs) {
        sidebarSections = await initializeLocalStorage();
      }
      sidebarSections[key] = toggleInput.checked;
      if (typeof window.saveSidebarState === "function") {
        window.saveSidebarState(sidebarSections);
      }
    }
  }

  /**
   * Restores toggle states from storage, delegating to the sidebarState
   * helper that applied them before the document finished loading.
   * @function restoreToggleStates
   * @returns {void}
   */
  function restoreToggleStates() {
    if (typeof window.applySidebarState === "function") {
      window.applySidebarState();
    }
  }

  /**
   * Attaches delegated click listeners for toggle interactions on the sidebar.
   * @function addToggleListeners
   * @returns {void}
   */
  function addToggleListeners() {
    const handleToggle = (event) => {
      const id = event.target.id;
      if (!id.startsWith("toggle_sb_")) { return; }
      if (typeof window.setSidebarSectionState === "function") {
        window.setSidebarSectionState(event.target, event.target.checked);
      }
      saveToggleStates();
      stickySidebar();
    };
    // Keep exactly one delegated handler on sb_wrap at any time. The handler
    // is swapped each call so its captured element references are always the
    // freshest (refreshAll replaces the toggle nodes inside #xhr), while the
    // listener count never grows.
    if (listenersTarget && listenersHandler) {
      listenersTarget.removeEventListener("click", listenersHandler);
    }
    sb_wrap.addEventListener("click", handleToggle);
    listenersTarget = sb_wrap;
    listenersHandler = handleToggle;
  }

  restoreToggleStates();
  addToggleListeners();
}

/**
 * Counts news items in the sidebar and updates the news badge display.
 * @function countNewsItems
 * @returns {void}
 */
function countNewsItems() {
  const sbNewsHeadings = document.getElementById("sb_newsheadings");
  const newsBadge = document.getElementById("newsCount");
  const doubleCount = sb.querySelector("#newsCount+#newsCount");
  if (!sbNewsHeadings || !newsBadge) { return; }
  if (doubleCount) { doubleCount.remove(); }
  const newsCount = sbNewsHeadings.querySelectorAll("table tr").length;
  newsBadge.hidden = newsCount <= 0 || !sbNewsHeadings.classList.contains("collapsed");
  if (newsCount > 0 && newsBadge.innerHTML !== newsCount.toString()) {
    newsBadge.innerHTML = newsCount;
  }
}

export { sectionToggler, countNewsItems };