Add help navigation search, mobile toggle, and scroll spy
This commit is contained in:
+141
-63
@@ -211,57 +211,161 @@ function initHelpModal() {
|
||||
}
|
||||
|
||||
function initHelpNavigation() {
|
||||
const helpContent = document.querySelector(".help-content");
|
||||
const helpBody = document.getElementById("help-body");
|
||||
const navLinks = document.querySelectorAll(".help-nav-link");
|
||||
const navList = document.querySelector(".help-nav-list");
|
||||
const navList = document.getElementById("help-nav-list");
|
||||
const nav = document.getElementById("help-nav");
|
||||
const searchInput = document.getElementById("help-nav-search");
|
||||
const toggleBtn = document.getElementById("help-nav-toggle");
|
||||
|
||||
if (!helpContent || !helpBody || !navLinks.length) return;
|
||||
if (!helpBody || !navList || !nav) return;
|
||||
|
||||
// Handle nav link clicks
|
||||
navLinks.forEach((link) => {
|
||||
const allLinks = [...navList.querySelectorAll(".help-nav-link")];
|
||||
|
||||
// ── Scroll to section and focus it ──
|
||||
function scrollToSection(targetId) {
|
||||
const section = document.getElementById(targetId);
|
||||
if (!section) return;
|
||||
section.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
section.setAttribute("tabindex", "-1");
|
||||
section.focus({ preventScroll: true });
|
||||
// On mobile, collapse nav after selection
|
||||
if (window.innerWidth <= 768 && nav) {
|
||||
nav.classList.add("collapsed");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Nav link click handler ──
|
||||
allLinks.forEach((link) => {
|
||||
link.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const targetId = link.getAttribute("href").substring(1);
|
||||
const targetSection = document.getElementById(targetId);
|
||||
if (targetSection) {
|
||||
targetSection.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
// Focus the section for accessibility
|
||||
targetSection.setAttribute("tabindex", "-1");
|
||||
targetSection.focus({ preventScroll: true });
|
||||
}
|
||||
scrollToSection(targetId);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Keyboard navigation inside nav list ──
|
||||
if (navList) {
|
||||
navList.addEventListener("keydown", (e) => {
|
||||
const items = [...navList.querySelectorAll(".help-nav-link")];
|
||||
const currentIdx = items.indexOf(document.activeElement);
|
||||
navList.addEventListener("keydown", (e) => {
|
||||
const items = [...navList.querySelectorAll(".help-nav-link:not(.hidden)")];
|
||||
if (!items.length) return;
|
||||
const currentIdx = items.indexOf(document.activeElement);
|
||||
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
const next = (currentIdx + 1) % items.length;
|
||||
items[next].focus();
|
||||
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
const prev = (currentIdx - 1 + items.length) % items.length;
|
||||
items[prev].focus();
|
||||
} else if (e.key === "Home") {
|
||||
e.preventDefault();
|
||||
items[0].focus();
|
||||
} else if (e.key === "End") {
|
||||
e.preventDefault();
|
||||
items[items.length - 1].focus();
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
const next = (currentIdx + 1) % items.length;
|
||||
items[next].focus();
|
||||
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
const prev = (currentIdx - 1 + items.length) % items.length;
|
||||
items[prev].focus();
|
||||
} else if (e.key === "Home") {
|
||||
e.preventDefault();
|
||||
items[0].focus();
|
||||
} else if (e.key === "End") {
|
||||
e.preventDefault();
|
||||
items[items.length - 1].focus();
|
||||
} else if (e.key === "Enter" && currentIdx >= 0) {
|
||||
e.preventDefault();
|
||||
const targetId = items[currentIdx].getAttribute("href").substring(1);
|
||||
scrollToSection(targetId);
|
||||
}
|
||||
});
|
||||
|
||||
// Make nav items focusable
|
||||
navList.querySelectorAll("li").forEach((li) => li.setAttribute("role", "none"));
|
||||
|
||||
// ── Search filter ──
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", () => {
|
||||
const q = searchInput.value.toLowerCase().trim();
|
||||
allLinks.forEach((link) => {
|
||||
const text = link.textContent.toLowerCase();
|
||||
if (!q || text.includes(q)) {
|
||||
link.classList.remove("hidden");
|
||||
} else {
|
||||
link.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
// Update active state for visible links
|
||||
updateActiveLink();
|
||||
});
|
||||
// Clear search on Escape
|
||||
searchInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
searchInput.value = "";
|
||||
searchInput.dispatchEvent(new Event("input"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Make nav items focusable
|
||||
navList.querySelectorAll("li").forEach((li) => {
|
||||
li.setAttribute("role", "none"); // remove listitem role for cleaner a11y
|
||||
// ── Mobile toggle ──
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener("click", () => {
|
||||
nav.classList.toggle("collapsed");
|
||||
});
|
||||
// Start collapsed on mobile
|
||||
if (window.innerWidth <= 768) {
|
||||
nav.classList.add("collapsed");
|
||||
}
|
||||
window.addEventListener("resize", () => {
|
||||
if (window.innerWidth > 768) {
|
||||
nav.classList.remove("collapsed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Scroll spy (IntersectionObserver) ──
|
||||
let observer;
|
||||
function updateActiveLink() {
|
||||
// Find the first visible section and highlight its link
|
||||
const sections = [...document.querySelectorAll(".help-section")];
|
||||
let activeId = null;
|
||||
for (const section of sections) {
|
||||
const rect = section.getBoundingClientRect();
|
||||
const helpContent = document.querySelector(".help-content");
|
||||
const contentRect = helpContent ? helpContent.getBoundingClientRect() : null;
|
||||
// Section top is in the upper half of the viewport (or within content)
|
||||
if (rect.top < (contentRect ? contentRect.top + 120 : 200)) {
|
||||
activeId = section.id;
|
||||
}
|
||||
}
|
||||
if (activeId) {
|
||||
allLinks.forEach((link) => {
|
||||
const isMatch = link.getAttribute("href") === `#${activeId}`;
|
||||
link.classList.toggle("active", isMatch);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
// Find the entry most visible near the top
|
||||
let bestEntry = null;
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
if (!bestEntry || entry.boundingClientRect.top < bestEntry.boundingClientRect.top) {
|
||||
bestEntry = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestEntry) {
|
||||
const id = bestEntry.target.getAttribute("id");
|
||||
allLinks.forEach((link) => {
|
||||
const isMatch = link.getAttribute("href") === `#${id}`;
|
||||
link.classList.toggle("active", isMatch);
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
root: helpBody,
|
||||
rootMargin: "-10% 0px -75% 0px",
|
||||
threshold: [0, 0.25, 0.5],
|
||||
}
|
||||
);
|
||||
|
||||
document.querySelectorAll(".help-section").forEach((section) => {
|
||||
observer.observe(section);
|
||||
});
|
||||
|
||||
// ── Keyboard: Ctrl+↑/↓ inside help body to jump between sections ──
|
||||
helpBody.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
||||
@@ -269,10 +373,11 @@ function initHelpNavigation() {
|
||||
const sections = [...document.querySelectorAll(".help-section")];
|
||||
const currentSection = sections.find((s) => {
|
||||
const rect = s.getBoundingClientRect();
|
||||
return rect.top >= 0 && rect.top < 100;
|
||||
const helpContent = document.querySelector(".help-content");
|
||||
const contentRect = helpContent ? helpContent.getBoundingClientRect() : null;
|
||||
return rect.top >= (contentRect ? contentRect.top : 0) && rect.top < (contentRect ? contentRect.top + 120 : 120);
|
||||
});
|
||||
if (!currentSection) return;
|
||||
|
||||
const currentIdx = sections.indexOf(currentSection);
|
||||
if (e.key === "ArrowDown" && currentIdx < sections.length - 1) {
|
||||
sections[currentIdx + 1].scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
@@ -281,33 +386,6 @@ function initHelpNavigation() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Scroll spy ──
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.getAttribute("id");
|
||||
navLinks.forEach((link) => {
|
||||
if (link.getAttribute("href") === `#${id}`) {
|
||||
navLinks.forEach((l) => l.classList.remove("active"));
|
||||
link.classList.add("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
root: helpBody,
|
||||
rootMargin: "-20% 0px -70% 0px",
|
||||
threshold: 0,
|
||||
},
|
||||
);
|
||||
|
||||
// Observe all sections
|
||||
document.querySelectorAll(".help-section").forEach((section) => {
|
||||
observer.observe(section);
|
||||
});
|
||||
}
|
||||
|
||||
function closeHelpModal() {
|
||||
|
||||
Reference in New Issue
Block a user