Help: mobile slide-over TOC with overlay, search content highlighting
This commit is contained in:
+79
-37
@@ -225,7 +225,9 @@ function initHelpNavigation() {
|
||||
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");
|
||||
const hamburger = document.getElementById("help-hamburger");
|
||||
const navClose = document.getElementById("help-nav-close");
|
||||
const overlay = document.getElementById("help-nav-overlay");
|
||||
const helpContent = document.querySelector(".help-content");
|
||||
|
||||
if (!helpBody || !navList || !nav) return;
|
||||
@@ -235,27 +237,41 @@ function initHelpNavigation() {
|
||||
const sectionMap = {};
|
||||
allSections.forEach(s => { sectionMap[s.id] = s; });
|
||||
|
||||
// Build text index for content search (section title + text content)
|
||||
// Build text index for content search
|
||||
const sectionTextCache = {};
|
||||
allSections.forEach(s => {
|
||||
sectionTextCache[s.id] = (s.textContent || '').toLowerCase();
|
||||
});
|
||||
|
||||
// ── Mobile: open/close TOC slide-over ──
|
||||
function openTOC() {
|
||||
nav.classList.add("open");
|
||||
if (overlay) overlay.classList.add("visible");
|
||||
if (searchInput) setTimeout(() => searchInput.focus(), 150);
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
function closeTOC() {
|
||||
nav.classList.remove("open");
|
||||
if (overlay) overlay.classList.remove("visible");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
function isMobile() { return window.innerWidth <= 768; }
|
||||
|
||||
if (hamburger) hamburger.addEventListener("click", openTOC);
|
||||
if (navClose) navClose.addEventListener("click", closeTOC);
|
||||
if (overlay) overlay.addEventListener("click", closeTOC);
|
||||
|
||||
// ── Scroll to section ──
|
||||
function scrollToSection(targetId) {
|
||||
const section = sectionMap[targetId];
|
||||
if (!section) return;
|
||||
section.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
section.setAttribute("tabindex", "-1");
|
||||
// On mobile, collapse nav after selection
|
||||
if (window.innerWidth <= 768 && nav) {
|
||||
nav.classList.add("collapsed");
|
||||
}
|
||||
if (isMobile()) closeTOC();
|
||||
}
|
||||
|
||||
// ── Focus the content pane (first heading in active section) ──
|
||||
// ── Focus the content pane ──
|
||||
function focusContent() {
|
||||
// Find the active section and focus its first heading
|
||||
const activeLink = navList.querySelector(".help-nav-link.active:not(.hidden)");
|
||||
if (activeLink) {
|
||||
const targetId = activeLink.getAttribute("href").substring(1);
|
||||
@@ -265,12 +281,12 @@ function initHelpNavigation() {
|
||||
if (h2) { h2.setAttribute("tabindex", "-1"); h2.focus({ preventScroll: true }); return; }
|
||||
}
|
||||
}
|
||||
// Fallback: focus the content area itself
|
||||
if (helpContent) { helpContent.setAttribute("tabindex", "-1"); helpContent.focus({ preventScroll: true }); }
|
||||
}
|
||||
|
||||
// ── Focus TOC active link ──
|
||||
function focusTOC() {
|
||||
if (isMobile() && !nav.classList.contains("open")) openTOC();
|
||||
const activeLink = navList.querySelector(".help-nav-link.active:not(.hidden)");
|
||||
if (activeLink) { activeLink.focus(); return; }
|
||||
const firstLink = navList.querySelector(".help-nav-link:not(.hidden)");
|
||||
@@ -301,11 +317,9 @@ function initHelpNavigation() {
|
||||
const prev = (currentIdx - 1 + items.length) % items.length;
|
||||
items[prev].focus();
|
||||
} else if (e.key === "ArrowRight") {
|
||||
// Right arrow: go to content pane
|
||||
e.preventDefault();
|
||||
focusContent();
|
||||
} else if (e.key === "ArrowLeft") {
|
||||
// Left arrow in TOC: stay (prev item)
|
||||
e.preventDefault();
|
||||
const prev = (currentIdx - 1 + items.length) % items.length;
|
||||
items[prev].focus();
|
||||
@@ -319,12 +333,10 @@ function initHelpNavigation() {
|
||||
e.preventDefault();
|
||||
const targetId = items[currentIdx].getAttribute("href").substring(1);
|
||||
scrollToSection(targetId);
|
||||
// After scrolling, move focus to content
|
||||
setTimeout(() => focusContent(), 400);
|
||||
}
|
||||
});
|
||||
|
||||
// Make nav items focusable
|
||||
navList.querySelectorAll("li").forEach((li) => li.setAttribute("role", "none"));
|
||||
|
||||
// ── Keyboard navigation in content pane ──
|
||||
@@ -332,7 +344,6 @@ function initHelpNavigation() {
|
||||
helpContent.setAttribute("tabindex", "-1");
|
||||
helpContent.addEventListener("keydown", (e) => {
|
||||
if (e.key === "ArrowLeft") {
|
||||
// Left arrow in content: go back to TOC
|
||||
e.preventDefault();
|
||||
focusTOC();
|
||||
} else if (e.key === "ArrowUp" && document.activeElement === helpContent) {
|
||||
@@ -353,7 +364,7 @@ function initHelpNavigation() {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Search: filter TOC + hide non-matching content sections ──
|
||||
// ── Search: filter TOC + hide non-matching sections + highlight text ──
|
||||
let currentQuery = "";
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", () => {
|
||||
@@ -369,8 +380,57 @@ function initHelpNavigation() {
|
||||
});
|
||||
}
|
||||
|
||||
function clearHighlights() {
|
||||
allSections.forEach(section => {
|
||||
section.querySelectorAll(".help-search-highlight").forEach(el => {
|
||||
const parent = el.parentNode;
|
||||
parent.replaceChild(document.createTextNode(el.textContent), el);
|
||||
parent.normalize();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function highlightText(root, query) {
|
||||
if (!query || query.length < 2) return;
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
|
||||
const textNodes = [];
|
||||
while (walker.nextNode()) {
|
||||
const node = walker.currentNode;
|
||||
// Skip nodes inside script, style, or already-highlighted
|
||||
if (node.parentElement && !node.parentElement.closest("script, style, .help-search-highlight, .help-nav")) {
|
||||
textNodes.push(node);
|
||||
}
|
||||
}
|
||||
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||
textNodes.forEach(node => {
|
||||
const text = node.textContent;
|
||||
if (!regex.test(text)) return;
|
||||
regex.lastIndex = 0;
|
||||
const frag = document.createDocumentFragment();
|
||||
let lastIdx = 0;
|
||||
let match;
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
if (match.index > lastIdx) {
|
||||
frag.appendChild(document.createTextNode(text.slice(lastIdx, match.index)));
|
||||
}
|
||||
const mark = document.createElement("mark");
|
||||
mark.className = "help-search-highlight";
|
||||
mark.textContent = match[0];
|
||||
frag.appendChild(mark);
|
||||
lastIdx = regex.lastIndex;
|
||||
if (match[0].length === 0) regex.lastIndex++;
|
||||
}
|
||||
if (lastIdx < text.length) {
|
||||
frag.appendChild(document.createTextNode(text.slice(lastIdx)));
|
||||
}
|
||||
node.parentNode.replaceChild(frag, node);
|
||||
});
|
||||
}
|
||||
|
||||
function applySearchFilter() {
|
||||
const q = currentQuery;
|
||||
// Clear previous highlights
|
||||
clearHighlights();
|
||||
// Filter TOC links
|
||||
allLinks.forEach((link) => {
|
||||
const text = link.textContent.toLowerCase();
|
||||
@@ -380,7 +440,7 @@ function initHelpNavigation() {
|
||||
link.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
// Filter content sections
|
||||
// Filter content sections + highlight
|
||||
allSections.forEach((section) => {
|
||||
if (!q) {
|
||||
section.classList.remove("help-hidden");
|
||||
@@ -390,12 +450,13 @@ function initHelpNavigation() {
|
||||
const sectionTitle = (section.querySelector("h2, h3")?.textContent || '').toLowerCase();
|
||||
if (sectionTitle.includes(q) || sectionText.includes(q)) {
|
||||
section.classList.remove("help-hidden");
|
||||
// Highlight matching text in this section
|
||||
highlightText(section, q);
|
||||
} else {
|
||||
section.classList.add("help-hidden");
|
||||
}
|
||||
});
|
||||
updateActiveLink();
|
||||
// If no visible results, show a message
|
||||
ensureNoResultsMessage(q);
|
||||
}
|
||||
|
||||
@@ -415,26 +476,7 @@ function initHelpNavigation() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mobile toggle ──
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener("click", () => {
|
||||
const wasCollapsed = nav.classList.contains("collapsed");
|
||||
nav.classList.toggle("collapsed");
|
||||
// If expanding, focus search
|
||||
if (wasCollapsed && searchInput) {
|
||||
setTimeout(() => searchInput.focus(), 150);
|
||||
}
|
||||
});
|
||||
// Start expanded on mobile so user sees the TOC immediately
|
||||
// (we use collapsed only after user taps a section or explicitly toggles)
|
||||
window.addEventListener("resize", () => {
|
||||
if (window.innerWidth > 768) {
|
||||
nav.classList.remove("collapsed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Scroll spy (IntersectionObserver) ──
|
||||
// ── Scroll spy ──
|
||||
function updateActiveLink() {
|
||||
const visibleLinks = allLinks.filter(l => !l.classList.contains("hidden"));
|
||||
const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden"));
|
||||
|
||||
Reference in New Issue
Block a user