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, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
},
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.