chunk-042-faq.js

I’ll build this as a standalone advanced FAQ JavaScript chunk for RX Theme: safe DOM handling, accordion behavior, accessibility, FAQ schema JSON-LD, search/filter, deep links, analytics hooks, and WordPress-friendly initialization.

Below is a strong advanced-level file for:

assets/static-js/static-chunks-js/chunk-042-faq.js

It includes FAQ accordion, accessibility, keyboard navigation, FAQ schema, search, category filter, deep link support, copy FAQ link, expand/collapse all, reading progress, localStorage memory, analytics hooks, and safe WordPress-friendly loading.

/*!
 * RX Theme - FAQ Advanced Chunk
 * File: assets/static-js/static-chunks-js/chunk-042-faq.js
 * Purpose: Advanced FAQ accordion, schema, search, filters, accessibility, deep links
 * Author: RX Theme
 */

(function () {
  "use strict";

  /**
   * ============================================================
   * RX FAQ CONFIG
   * ============================================================
   */

  const RX_FAQ_CONFIG = {
    rootSelector: "[data-rx-faq], .rx-faq, .rx-faq-section",
    itemSelector: "[data-rx-faq-item], .rx-faq-item",
    questionSelector: "[data-rx-faq-question], .rx-faq-question",
    answerSelector: "[data-rx-faq-answer], .rx-faq-answer",

    searchSelector: "[data-rx-faq-search], .rx-faq-search",
    filterSelector: "[data-rx-faq-filter], .rx-faq-filter",
    expandAllSelector: "[data-rx-faq-expand-all], .rx-faq-expand-all",
    collapseAllSelector: "[data-rx-faq-collapse-all], .rx-faq-collapse-all",
    resetSelector: "[data-rx-faq-reset], .rx-faq-reset",

    openClass: "is-open",
    activeClass: "is-active",
    hiddenClass: "is-hidden",
    noResultClass: "has-no-result",
    readyClass: "rx-faq-ready",

    allowMultipleOpen: true,
    rememberOpenItems: true,
    enableSchema: true,
    enableDeepLink: true,
    enableCopyLink: true,
    enableKeyboard: true,
    enableAnalyticsEvent: true,
    enableReadingProgress: true,
    enableAutoId: true,

    storageKey: "rx_theme_faq_open_items",
    animationDuration: 260,
    searchDebounce: 180,
    scrollOffset: 90
  };

  /**
   * ============================================================
   * SMALL HELPERS
   * ============================================================
   */

  const RXFAQ = {
    roots: [],
    initialized: false,
    uid: 0,
    searchTimers: new WeakMap(),

    qs(selector, parent = document) {
      return parent.querySelector(selector);
    },

    qsa(selector, parent = document) {
      return Array.prototype.slice.call(parent.querySelectorAll(selector));
    },

    hasClass(el, className) {
      return el && el.classList && el.classList.contains(className);
    },

    addClass(el, className) {
      if (el && el.classList) el.classList.add(className);
    },

    removeClass(el, className) {
      if (el && el.classList) el.classList.remove(className);
    },

    toggleClass(el, className, force) {
      if (el && el.classList) el.classList.toggle(className, force);
    },

    attr(el, name, value) {
      if (!el) return null;
      if (typeof value === "undefined") return el.getAttribute(name);
      el.setAttribute(name, value);
      return value;
    },

    removeAttr(el, name) {
      if (el) el.removeAttribute(name);
    },

    text(el) {
      return (el && el.textContent ? el.textContent : "").replace(/\s+/g, " ").trim();
    },

    slugify(text) {
      return String(text || "")
        .toLowerCase()
        .trim()
        .replace(/&/g, " and ")
        .replace(/[^\w\s-]/g, "")
        .replace(/\s+/g, "-")
        .replace(/-+/g, "-")
        .replace(/^-|-$/g, "");
    },

    escapeHTML(value) {
      return String(value || "")
        .replace(/&/g, "&")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
    },

    normalize(value) {
      return String(value || "")
        .toLowerCase()
        .replace(/\s+/g, " ")
        .trim();
    },

    debounce(root, callback, delay) {
      const oldTimer = RXFAQ.searchTimers.get(root);
      if (oldTimer) clearTimeout(oldTimer);

      const timer = setTimeout(callback, delay);
      RXFAQ.searchTimers.set(root, timer);
    },

    isVisible(el) {
      return !!(el && el.offsetParent !== null);
    },

    prefersReducedMotion() {
      return window.matchMedia &&
        window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    },

    getFocusable(container) {
      if (!container) return [];
      return RXFAQ.qsa(
        'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
        container
      );
    },

    getItemId(item, question) {
      if (item.id) return item.id;

      let text = RXFAQ.text(question);
      let slug = RXFAQ.slugify(text);

      if (!slug) {
        RXFAQ.uid += 1;
        slug = "rx-faq-item-" + RXFAQ.uid;
      }

      let finalId = slug;
      let count = 2;

      while (document.getElementById(finalId)) {
        finalId = slug + "-" + count;
        count += 1;
      }

      item.id = finalId;
      return finalId;
    },

    dispatch(name, detail = {}) {
      if (!RX_FAQ_CONFIG.enableAnalyticsEvent) return;

      const event = new CustomEvent(name, {
        bubbles: true,
        cancelable: false,
        detail
      });

      document.dispatchEvent(event);

      if (window.dataLayer && Array.isArray(window.dataLayer)) {
        window.dataLayer.push({
          event: name,
          rx_faq: detail
        });
      }
    }
  };

  /**
   * ============================================================
   * ACCESSIBILITY SETUP
   * ============================================================
   */

  function setupFAQAccessibility(root) {
    const items = RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root);

    items.forEach((item, index) => {
      const question = RXFAQ.qs(RX_FAQ_CONFIG.questionSelector, item);
      const answer = RXFAQ.qs(RX_FAQ_CONFIG.answerSelector, item);

      if (!question || !answer) return;

      const itemId = RXFAQ.getItemId(item, question);
      const questionId = itemId + "-question";
      const answerId = itemId + "-answer";

      if (!question.id) question.id = questionId;
      if (!answer.id) answer.id = answerId;

      if (question.tagName.toLowerCase() !== "button") {
        question.setAttribute("role", "button");
        question.setAttribute("tabindex", "0");
      }

      question.setAttribute("aria-controls", answer.id);
      question.setAttribute("aria-expanded", RXFAQ.hasClass(item, RX_FAQ_CONFIG.openClass) ? "true" : "false");

      answer.setAttribute("role", "region");
      answer.setAttribute("aria-labelledby", question.id);

      if (!RXFAQ.hasClass(item, RX_FAQ_CONFIG.openClass)) {
        answer.hidden = true;
      }

      item.setAttribute("data-rx-faq-index", String(index));
    });
  }

  /**
   * ============================================================
   * OPEN / CLOSE FUNCTIONS
   * ============================================================
   */

  function openFAQItem(item, options = {}) {
    if (!item) return;

    const root = item.closest(RX_FAQ_CONFIG.rootSelector);
    const question = RXFAQ.qs(RX_FAQ_CONFIG.questionSelector, item);
    const answer = RXFAQ.qs(RX_FAQ_CONFIG.answerSelector, item);

    if (!root || !question || !answer) return;

    if (!RX_FAQ_CONFIG.allowMultipleOpen && !options.skipCloseOthers) {
      closeAllFAQItems(root, item);
    }

    RXFAQ.addClass(item, RX_FAQ_CONFIG.openClass);
    question.setAttribute("aria-expanded", "true");

    animateOpen(answer);

    saveOpenItems(root);

    RXFAQ.dispatch("rx_faq_open", {
      id: item.id || "",
      question: RXFAQ.text(question)
    });
  }

  function closeFAQItem(item) {
    if (!item) return;

    const root = item.closest(RX_FAQ_CONFIG.rootSelector);
    const question = RXFAQ.qs(RX_FAQ_CONFIG.questionSelector, item);
    const answer = RXFAQ.qs(RX_FAQ_CONFIG.answerSelector, item);

    if (!root || !question || !answer) return;

    RXFAQ.removeClass(item, RX_FAQ_CONFIG.openClass);
    question.setAttribute("aria-expanded", "false");

    animateClose(answer);

    saveOpenItems(root);

    RXFAQ.dispatch("rx_faq_close", {
      id: item.id || "",
      question: RXFAQ.text(question)
    });
  }

  function toggleFAQItem(item) {
    if (!item) return;

    if (RXFAQ.hasClass(item, RX_FAQ_CONFIG.openClass)) {
      closeFAQItem(item);
    } else {
      openFAQItem(item);
    }
  }

  function openAllFAQItems(root) {
    RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root).forEach((item) => {
      if (!RXFAQ.hasClass(item, RX_FAQ_CONFIG.hiddenClass)) {
        openFAQItem(item, { skipCloseOthers: true });
      }
    });

    RXFAQ.dispatch("rx_faq_expand_all", {
      root: root.id || ""
    });
  }

  function closeAllFAQItems(root, exceptItem = null) {
    RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root).forEach((item) => {
      if (exceptItem && item === exceptItem) return;
      closeFAQItem(item);
    });

    RXFAQ.dispatch("rx_faq_collapse_all", {
      root: root.id || ""
    });
  }

  /**
   * ============================================================
   * ANIMATION
   * ============================================================
   */

  function animateOpen(answer) {
    if (!answer) return;

    answer.hidden = false;

    if (RXFAQ.prefersReducedMotion()) {
      answer.style.height = "";
      answer.style.overflow = "";
      return;
    }

    answer.style.overflow = "hidden";
    answer.style.height = "0px";

    const height = answer.scrollHeight;

    requestAnimationFrame(() => {
      answer.style.transition = "height " + RX_FAQ_CONFIG.animationDuration + "ms ease";
      answer.style.height = height + "px";
    });

    window.setTimeout(() => {
      answer.style.height = "";
      answer.style.overflow = "";
      answer.style.transition = "";
    }, RX_FAQ_CONFIG.animationDuration + 30);
  }

  function animateClose(answer) {
    if (!answer) return;

    if (RXFAQ.prefersReducedMotion()) {
      answer.hidden = true;
      answer.style.height = "";
      answer.style.overflow = "";
      return;
    }

    answer.style.overflow = "hidden";
    answer.style.height = answer.scrollHeight + "px";

    requestAnimationFrame(() => {
      answer.style.transition = "height " + RX_FAQ_CONFIG.animationDuration + "ms ease";
      answer.style.height = "0px";
    });

    window.setTimeout(() => {
      answer.hidden = true;
      answer.style.height = "";
      answer.style.overflow = "";
      answer.style.transition = "";
    }, RX_FAQ_CONFIG.animationDuration + 30);
  }

  /**
   * ============================================================
   * SEARCH AND FILTER
   * ============================================================
   */

  function setupFAQSearch(root) {
    const searchInput = RXFAQ.qs(RX_FAQ_CONFIG.searchSelector, root) ||
      RXFAQ.qs(RX_FAQ_CONFIG.searchSelector);

    if (!searchInput) return;

    searchInput.setAttribute("autocomplete", "off");
    searchInput.setAttribute("spellcheck", "true");

    searchInput.addEventListener("input", () => {
      RXFAQ.debounce(root, () => {
        applyFAQSearchAndFilter(root);
      }, RX_FAQ_CONFIG.searchDebounce);
    });
  }

  function setupFAQFilter(root) {
    const filter = RXFAQ.qs(RX_FAQ_CONFIG.filterSelector, root) ||
      RXFAQ.qs(RX_FAQ_CONFIG.filterSelector);

    if (!filter) return;

    filter.addEventListener("change", () => {
      applyFAQSearchAndFilter(root);
    });

    filter.addEventListener("click", (event) => {
      const target = event.target.closest("[data-rx-faq-filter-value]");
      if (!target) return;

      event.preventDefault();

      const value = target.getAttribute("data-rx-faq-filter-value") || "";
      filter.setAttribute("data-rx-current-filter", value);

      RXFAQ.qsa("[data-rx-faq-filter-value]", filter).forEach((button) => {
        button.setAttribute("aria-pressed", button === target ? "true" : "false");
        RXFAQ.toggleClass(button, RX_FAQ_CONFIG.activeClass, button === target);
      });

      applyFAQSearchAndFilter(root);
    });
  }

  function getCurrentFilter(root) {
    const filter = RXFAQ.qs(RX_FAQ_CONFIG.filterSelector, root) ||
      RXFAQ.qs(RX_FAQ_CONFIG.filterSelector);

    if (!filter) return "";

    if (filter.tagName.toLowerCase() === "select") {
      return filter.value || "";
    }

    return filter.getAttribute("data-rx-current-filter") || "";
  }

  function getCurrentSearch(root) {
    const searchInput = RXFAQ.qs(RX_FAQ_CONFIG.searchSelector, root) ||
      RXFAQ.qs(RX_FAQ_CONFIG.searchSelector);

    return searchInput ? RXFAQ.normalize(searchInput.value) : "";
  }

  function applyFAQSearchAndFilter(root) {
    const searchValue = getCurrentSearch(root);
    const filterValue = RXFAQ.normalize(getCurrentFilter(root));
    const items = RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root);

    let visibleCount = 0;

    items.forEach((item) => {
      const question = RXFAQ.qs(RX_FAQ_CONFIG.questionSelector, item);
      const answer = RXFAQ.qs(RX_FAQ_CONFIG.answerSelector, item);

      const questionText = RXFAQ.normalize(RXFAQ.text(question));
      const answerText = RXFAQ.normalize(RXFAQ.text(answer));
      const category = RXFAQ.normalize(item.getAttribute("data-rx-faq-category") || "");

      const matchSearch = !searchValue ||
        questionText.includes(searchValue) ||
        answerText.includes(searchValue);

      const matchFilter = !filterValue ||
        filterValue === "all" ||
        category === filterValue;

      const shouldShow = matchSearch && matchFilter;

      RXFAQ.toggleClass(item, RX_FAQ_CONFIG.hiddenClass, !shouldShow);
      item.hidden = !shouldShow;

      if (shouldShow) {
        visibleCount += 1;
        if (searchValue) highlightFAQText(item, searchValue);
        else removeFAQHighlight(item);
      } else {
        removeFAQHighlight(item);
      }
    });

    RXFAQ.toggleClass(root, RX_FAQ_CONFIG.noResultClass, visibleCount === 0);

    const countTarget = RXFAQ.qs("[data-rx-faq-result-count]", root);
    if (countTarget) {
      countTarget.textContent = String(visibleCount);
    }

    RXFAQ.dispatch("rx_faq_search_filter", {
      search: searchValue,
      filter: filterValue,
      visibleCount
    });
  }

  function highlightFAQText(item, keyword) {
    if (!keyword || keyword.length < 2) return;

    const question = RXFAQ.qs(RX_FAQ_CONFIG.questionSelector, item);
    const answer = RXFAQ.qs(RX_FAQ_CONFIG.answerSelector, item);

    [question, answer].forEach((el) => {
      if (!el) return;

      if (!el.getAttribute("data-rx-original-html")) {
        el.setAttribute("data-rx-original-html", el.innerHTML);
      }

      const original = el.getAttribute("data-rx-original-html");
      const safeKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
      const regex = new RegExp("(" + safeKeyword + ")", "gi");

      el.innerHTML = original.replace(regex, "<mark class=\"rx-faq-highlight\">$1</mark>");
    });
  }

  function removeFAQHighlight(item) {
    const question = RXFAQ.qs(RX_FAQ_CONFIG.questionSelector, item);
    const answer = RXFAQ.qs(RX_FAQ_CONFIG.answerSelector, item);

    [question, answer].forEach((el) => {
      if (!el) return;

      const original = el.getAttribute("data-rx-original-html");
      if (original) {
        el.innerHTML = original;
        el.removeAttribute("data-rx-original-html");
      }
    });
  }

  /**
   * ============================================================
   * KEYBOARD SUPPORT
   * ============================================================
   */

  function setupFAQKeyboard(root) {
    if (!RX_FAQ_CONFIG.enableKeyboard) return;

    root.addEventListener("keydown", (event) => {
      const question = event.target.closest(RX_FAQ_CONFIG.questionSelector);
      if (!question || !root.contains(question)) return;

      const item = question.closest(RX_FAQ_CONFIG.itemSelector);
      const questions = RXFAQ.qsa(RX_FAQ_CONFIG.questionSelector, root)
        .filter((q) => !q.closest(RX_FAQ_CONFIG.itemSelector).hidden);

      const currentIndex = questions.indexOf(question);

      if (event.key === "Enter" || event.key === " ") {
        event.preventDefault();
        toggleFAQItem(item);
      }

      if (event.key === "ArrowDown") {
        event.preventDefault();
        const next = questions[currentIndex + 1] || questions[0];
        if (next) next.focus();
      }

      if (event.key === "ArrowUp") {
        event.preventDefault();
        const prev = questions[currentIndex - 1] || questions[questions.length - 1];
        if (prev) prev.focus();
      }

      if (event.key === "Home") {
        event.preventDefault();
        if (questions[0]) questions[0].focus();
      }

      if (event.key === "End") {
        event.preventDefault();
        if (questions[questions.length - 1]) questions[questions.length - 1].focus();
      }

      if (event.key === "Escape") {
        closeFAQItem(item);
        question.focus();
      }
    });
  }

  /**
   * ============================================================
   * CLICK EVENTS
   * ============================================================
   */

  function setupFAQClicks(root) {
    root.addEventListener("click", (event) => {
      const question = event.target.closest(RX_FAQ_CONFIG.questionSelector);

      if (question && root.contains(question)) {
        const item = question.closest(RX_FAQ_CONFIG.itemSelector);
        event.preventDefault();
        toggleFAQItem(item);
        return;
      }

      const copyButton = event.target.closest("[data-rx-faq-copy-link]");
      if (copyButton && root.contains(copyButton)) {
        event.preventDefault();
        copyFAQLink(copyButton);
        return;
      }
    });

    const expandAll = RXFAQ.qs(RX_FAQ_CONFIG.expandAllSelector, root) ||
      RXFAQ.qs(RX_FAQ_CONFIG.expandAllSelector);

    const collapseAll = RXFAQ.qs(RX_FAQ_CONFIG.collapseAllSelector, root) ||
      RXFAQ.qs(RX_FAQ_CONFIG.collapseAllSelector);

    const reset = RXFAQ.qs(RX_FAQ_CONFIG.resetSelector, root) ||
      RXFAQ.qs(RX_FAQ_CONFIG.resetSelector);

    if (expandAll) {
      expandAll.addEventListener("click", (event) => {
        event.preventDefault();
        openAllFAQItems(root);
      });
    }

    if (collapseAll) {
      collapseAll.addEventListener("click", (event) => {
        event.preventDefault();
        closeAllFAQItems(root);
      });
    }

    if (reset) {
      reset.addEventListener("click", (event) => {
        event.preventDefault();
        resetFAQ(root);
      });
    }
  }

  /**
   * ============================================================
   * DEEP LINK SUPPORT
   * ============================================================
   */

  function setupDeepLink(root) {
    if (!RX_FAQ_CONFIG.enableDeepLink) return;

    const hash = decodeURIComponent(window.location.hash || "").replace("#", "");
    if (!hash) return;

    const target = root.querySelector("#" + CSS.escape(hash));
    if (!target) return;

    const item = target.matches(RX_FAQ_CONFIG.itemSelector)
      ? target
      : target.closest(RX_FAQ_CONFIG.itemSelector);

    if (!item) return;

    openFAQItem(item);

    window.setTimeout(() => {
      scrollToFAQItem(item);
    }, 80);
  }

  function updateDeepLink(item) {
    if (!RX_FAQ_CONFIG.enableDeepLink || !item || !item.id) return;

    const url = new URL(window.location.href);
    url.hash = item.id;

    if (window.history && window.history.replaceState) {
      window.history.replaceState(null, "", url.toString());
    }
  }

  function scrollToFAQItem(item) {
    const rect = item.getBoundingClientRect();
    const top = window.scrollY + rect.top - RX_FAQ_CONFIG.scrollOffset;

    window.scrollTo({
      top,
      behavior: RXFAQ.prefersReducedMotion() ? "auto" : "smooth"
    });
  }

  /**
   * ============================================================
   * COPY LINK
   * ============================================================
   */

  function copyFAQLink(button) {
    const item = button.closest(RX_FAQ_CONFIG.itemSelector);
    if (!item) return;

    if (!item.id) {
      const question = RXFAQ.qs(RX_FAQ_CONFIG.questionSelector, item);
      RXFAQ.getItemId(item, question);
    }

    const url = new URL(window.location.href);
    url.hash = item.id;

    const link = url.toString();

    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(link).then(() => {
        showCopiedState(button);
      }).catch(() => {
        fallbackCopy(link, button);
      });
    } else {
      fallbackCopy(link, button);
    }

    RXFAQ.dispatch("rx_faq_copy_link", {
      id: item.id,
      url: link
    });
  }

  function fallbackCopy(text, button) {
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.setAttribute("readonly", "");
    textarea.style.position = "absolute";
    textarea.style.left = "-9999px";

    document.body.appendChild(textarea);
    textarea.select();

    try {
      document.execCommand("copy");
      showCopiedState(button);
    } catch (error) {
      console.warn("RX FAQ copy failed:", error);
    }

    document.body.removeChild(textarea);
  }

  function showCopiedState(button) {
    const oldText = button.textContent;
    button.textContent = button.getAttribute("data-rx-copied-text") || "Copied!";
    button.setAttribute("aria-live", "polite");

    window.setTimeout(() => {
      button.textContent = oldText;
    }, 1600);
  }

  /**
   * ============================================================
   * LOCAL STORAGE MEMORY
   * ============================================================
   */

  function getRootStorageId(root) {
    if (root.id) return root.id;

    const heading = RXFAQ.qs("h1, h2, h3", root);
    const slug = RXFAQ.slugify(RXFAQ.text(heading)) || "default";

    root.id = "rx-faq-" + slug;
    return root.id;
  }

  function saveOpenItems(root) {
    if (!RX_FAQ_CONFIG.rememberOpenItems) return;

    try {
      const rootId = getRootStorageId(root);
      const openIds = RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root)
        .filter((item) => RXFAQ.hasClass(item, RX_FAQ_CONFIG.openClass))
        .map((item) => item.id)
        .filter(Boolean);

      const all = JSON.parse(localStorage.getItem(RX_FAQ_CONFIG.storageKey) || "{}");
      all[rootId] = openIds;

      localStorage.setItem(RX_FAQ_CONFIG.storageKey, JSON.stringify(all));
    } catch (error) {
      // localStorage can be blocked; ignore safely.
    }
  }

  function restoreOpenItems(root) {
    if (!RX_FAQ_CONFIG.rememberOpenItems) return;

    try {
      const rootId = getRootStorageId(root);
      const all = JSON.parse(localStorage.getItem(RX_FAQ_CONFIG.storageKey) || "{}");
      const openIds = Array.isArray(all[rootId]) ? all[rootId] : [];

      openIds.forEach((id) => {
        const item = root.querySelector("#" + CSS.escape(id));
        if (item) openFAQItem(item, { skipCloseOthers: true });
      });
    } catch (error) {
      // localStorage can be blocked; ignore safely.
    }
  }

  /**
   * ============================================================
   * FAQ SCHEMA JSON-LD
   * ============================================================
   */

  function buildFAQSchema(root) {
    if (!RX_FAQ_CONFIG.enableSchema) return;
    if (root.getAttribute("data-rx-faq-schema") === "off") return;

    const items = RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root);
    const mainEntity = [];

    items.forEach((item) => {
      const question = RXFAQ.qs(RX_FAQ_CONFIG.questionSelector, item);
      const answer = RXFAQ.qs(RX_FAQ_CONFIG.answerSelector, item);

      const qText = RXFAQ.text(question);
      const aText = RXFAQ.text(answer);

      if (!qText || !aText) return;

      mainEntity.push({
        "@type": "Question",
        "name": qText,
        "acceptedAnswer": {
          "@type": "Answer",
          "text": aText
        }
      });
    });

    if (!mainEntity.length) return;

    const oldSchemaId = root.getAttribute("data-rx-faq-schema-id");
    if (oldSchemaId) {
      const oldSchema = document.getElementById(oldSchemaId);
      if (oldSchema) oldSchema.remove();
    }

    const schema = {
      "@context": "https://schema.org",
      "@type": "FAQPage",
      "mainEntity": mainEntity
    };

    const script = document.createElement("script");
    const id = "rx-faq-schema-" + getRootStorageId(root);

    script.type = "application/ld+json";
    script.id = id;
    script.textContent = JSON.stringify(schema);

    document.head.appendChild(script);
    root.setAttribute("data-rx-faq-schema-id", id);
  }

  /**
   * ============================================================
   * READING PROGRESS INSIDE ANSWER
   * ============================================================
   */

  function setupReadingProgress(root) {
    if (!RX_FAQ_CONFIG.enableReadingProgress) return;

    const items = RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root);

    items.forEach((item) => {
      const answer = RXFAQ.qs(RX_FAQ_CONFIG.answerSelector, item);
      if (!answer) return;

      let progress = RXFAQ.qs(".rx-faq-answer-progress", item);

      if (!progress) {
        progress = document.createElement("span");
        progress.className = "rx-faq-answer-progress";
        progress.setAttribute("aria-hidden", "true");
        item.insertBefore(progress, item.firstChild);
      }
    });

    window.addEventListener("scroll", () => {
      updateReadingProgress(root);
    }, { passive: true });

    updateReadingProgress(root);
  }

  function updateReadingProgress(root) {
    const items = RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root);

    items.forEach((item) => {
      const progress = RXFAQ.qs(".rx-faq-answer-progress", item);
      if (!progress) return;

      const rect = item.getBoundingClientRect();
      const windowHeight = window.innerHeight || document.documentElement.clientHeight;

      let ratio = 0;

      if (rect.top < windowHeight && rect.bottom > 0) {
        const visible = Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0);
        ratio = Math.max(0, Math.min(1, visible / rect.height));
      }

      progress.style.transform = "scaleX(" + ratio.toFixed(3) + ")";
    });
  }

  /**
   * ============================================================
   * RESET
   * ============================================================
   */

  function resetFAQ(root) {
    const searchInput = RXFAQ.qs(RX_FAQ_CONFIG.searchSelector, root) ||
      RXFAQ.qs(RX_FAQ_CONFIG.searchSelector);

    const filter = RXFAQ.qs(RX_FAQ_CONFIG.filterSelector, root) ||
      RXFAQ.qs(RX_FAQ_CONFIG.filterSelector);

    if (searchInput) searchInput.value = "";

    if (filter) {
      if (filter.tagName.toLowerCase() === "select") {
        filter.value = "";
      } else {
        filter.setAttribute("data-rx-current-filter", "");
        RXFAQ.qsa("[data-rx-faq-filter-value]", filter).forEach((button) => {
          button.setAttribute("aria-pressed", "false");
          RXFAQ.removeClass(button, RX_FAQ_CONFIG.activeClass);
        });
      }
    }

    RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root).forEach((item) => {
      item.hidden = false;
      RXFAQ.removeClass(item, RX_FAQ_CONFIG.hiddenClass);
      removeFAQHighlight(item);
    });

    RXFAQ.removeClass(root, RX_FAQ_CONFIG.noResultClass);

    const countTarget = RXFAQ.qs("[data-rx-faq-result-count]", root);
    if (countTarget) {
      countTarget.textContent = String(RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root).length);
    }

    RXFAQ.dispatch("rx_faq_reset", {
      root: root.id || ""
    });
  }

  /**
   * ============================================================
   * AUTO CREATE COPY BUTTONS
   * ============================================================
   */

  function setupCopyButtons(root) {
    if (!RX_FAQ_CONFIG.enableCopyLink) return;

    RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root).forEach((item) => {
      if (RXFAQ.qs("[data-rx-faq-copy-link]", item)) return;

      const question = RXFAQ.qs(RX_FAQ_CONFIG.questionSelector, item);
      if (!question) return;

      const button = document.createElement("button");
      button.type = "button";
      button.className = "rx-faq-copy-link";
      button.setAttribute("data-rx-faq-copy-link", "");
      button.setAttribute("data-rx-copied-text", "Copied!");
      button.setAttribute("aria-label", "Copy link to this FAQ");
      button.textContent = "Copy link";

      item.appendChild(button);
    });
  }

  /**
   * ============================================================
   * AUTO INDEX / META
   * ============================================================
   */

  function setupFAQMeta(root) {
    const items = RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root);
    root.setAttribute("data-rx-faq-total", String(items.length));

    const countTarget = RXFAQ.qs("[data-rx-faq-result-count]", root);
    if (countTarget) {
      countTarget.textContent = String(items.length);
    }

    items.forEach((item, index) => {
      item.setAttribute("data-rx-faq-number", String(index + 1));

      const question = RXFAQ.qs(RX_FAQ_CONFIG.questionSelector, item);
      if (!question) return;

      if (!question.getAttribute("data-rx-faq-original-title")) {
        question.setAttribute("data-rx-faq-original-title", RXFAQ.text(question));
      }
    });
  }

  /**
   * ============================================================
   * PUBLIC API
   * ============================================================
   */

  window.RXThemeFAQ = {
    init,
    open: openFAQItem,
    close: closeFAQItem,
    toggle: toggleFAQItem,
    openAll: openAllFAQItems,
    closeAll: closeAllFAQItems,
    reset: resetFAQ,
    rebuildSchema: buildFAQSchema,
    config: RX_FAQ_CONFIG
  };

  /**
   * ============================================================
   * INIT ONE ROOT
   * ============================================================
   */

  function initRoot(root) {
    if (!root || root.getAttribute("data-rx-faq-initialized") === "true") return;

    root.setAttribute("data-rx-faq-initialized", "true");

    setupFAQMeta(root);
    setupFAQAccessibility(root);
    setupCopyButtons(root);
    setupFAQClicks(root);
    setupFAQKeyboard(root);
    setupFAQSearch(root);
    setupFAQFilter(root);
    setupReadingProgress(root);
    restoreOpenItems(root);
    setupDeepLink(root);
    buildFAQSchema(root);

    RXFAQ.addClass(root, RX_FAQ_CONFIG.readyClass);

    RXFAQ.dispatch("rx_faq_ready", {
      root: root.id || "",
      total: RXFAQ.qsa(RX_FAQ_CONFIG.itemSelector, root).length
    });
  }

  /**
   * ============================================================
   * MAIN INIT
   * ============================================================
   */

  function init(context = document) {
    const roots = RXFAQ.qsa(RX_FAQ_CONFIG.rootSelector, context);

    if (!roots.length) return;

    roots.forEach((root) => {
      initRoot(root);
    });

    RXFAQ.roots = roots;
    RXFAQ.initialized = true;
  }

  /**
   * ============================================================
   * HASH CHANGE SUPPORT
   * ============================================================
   */

  window.addEventListener("hashchange", () => {
    RXFAQ.roots.forEach((root) => {
      setupDeepLink(root);
    });
  });

  /**
   * ============================================================
   * MUTATION OBSERVER FOR AJAX / DYNAMIC CONTENT
   * ============================================================
   */

  function setupMutationObserver() {
    if (!("MutationObserver" in window)) return;

    const observer = new MutationObserver((mutations) => {
      let shouldInit = false;

      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (!(node instanceof HTMLElement)) return;

          if (
            node.matches &&
            (
              node.matches(RX_FAQ_CONFIG.rootSelector) ||
              node.querySelector(RX_FAQ_CONFIG.rootSelector)
            )
          ) {
            shouldInit = true;
          }
        });
      });

      if (shouldInit) {
        init(document);
      }
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
  }

  /**
   * ============================================================
   * DOM READY
   * ============================================================
   */

  function ready(callback) {
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", callback);
    } else {
      callback();
    }
  }

  ready(() => {
    init(document);
    setupMutationObserver();
  });

})();

Example HTML structure for this file

<section class="rx-faq-section" data-rx-faq>
  <input class="rx-faq-search" data-rx-faq-search type="search" placeholder="Search FAQ...">

  <button class="rx-faq-expand-all" data-rx-faq-expand-all>Expand all</button>
  <button class="rx-faq-collapse-all" data-rx-faq-collapse-all>Collapse all</button>
  <button class="rx-faq-reset" data-rx-faq-reset>Reset</button>

  <p>Showing <span data-rx-faq-result-count></span> FAQs</p>

  <div class="rx-faq-item" data-rx-faq-item data-rx-faq-category="general">
    <button class="rx-faq-question" data-rx-faq-question>
      What is RX Theme?
    </button>

    <div class="rx-faq-answer" data-rx-faq-answer>
      RX Theme is a custom WordPress theme structure built for speed, SEO, readability, and medical content publishing.
    </div>
  </div>
</section>

Recommended enqueue in functions.php

function rx_theme_enqueue_faq_chunk() {
    wp_enqueue_script(
        'rx-theme-faq-chunk',
        get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-042-faq.js',
        array(),
        '1.0.0',
        true
    );
}
add_action( 'wp_enqueue_scripts', 'rx_theme_enqueue_faq_chunk' );

Use this file as your advanced FAQ controller. It is safe for normal WordPress pages, medical article FAQ blocks, dynamic AJAX content, and SEO FAQ schema output.

Leave a Reply

Your email address will not be published. Required fields are marked *