Help: mobile slide-over TOC with overlay, search content highlighting
This commit is contained in:
+6
-2
@@ -762,6 +762,9 @@
|
||||
<div class="editor-modal" id="help-modal">
|
||||
<div class="editor-container help-container">
|
||||
<div class="editor-header">
|
||||
<button class="help-hamburger" id="help-hamburger" title="Sommaire" aria-label="Afficher le sommaire">
|
||||
<i data-lucide="menu" style="width:18px;height:18px"></i>
|
||||
</button>
|
||||
<div class="editor-title" id="help-title">Guide d'utilisation ObsiGate</div>
|
||||
<div class="editor-actions">
|
||||
<button class="editor-btn" id="help-close" title="Fermer l'aide" aria-label="Fermer l'aide">
|
||||
@@ -769,12 +772,13 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-nav-overlay" id="help-nav-overlay"></div>
|
||||
<div class="editor-body help-body" id="help-body">
|
||||
<nav class="help-nav" id="help-nav">
|
||||
<div class="help-nav-header">
|
||||
<div class="help-nav-title">Sommaire</div>
|
||||
<button class="help-nav-toggle" id="help-nav-toggle" title="Afficher/Masquer le sommaire" aria-label="Menu sommaire">
|
||||
<i data-lucide="chevron-down" style="width:16px;height:16px"></i>
|
||||
<button class="help-nav-close" id="help-nav-close" title="Fermer" aria-label="Fermer le sommaire">
|
||||
<i data-lucide="x" style="width:16px;height:16px"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="help-nav-search-wrap">
|
||||
|
||||
+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"));
|
||||
|
||||
+78
-44
@@ -2178,6 +2178,24 @@ select {
|
||||
}
|
||||
|
||||
/* Help Navigation Menu */
|
||||
.help-hamburger {
|
||||
display: none;
|
||||
width: 36px; height: 36px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.help-hamburger:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.help-nav-overlay {
|
||||
display: none;
|
||||
}
|
||||
.help-nav {
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
@@ -2209,7 +2227,7 @@ select {
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.help-nav-toggle {
|
||||
.help-nav-close {
|
||||
display: none;
|
||||
width: 32px; height: 32px;
|
||||
padding: 0;
|
||||
@@ -2221,7 +2239,7 @@ select {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.help-nav-toggle:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.help-nav-close:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.help-nav-search-wrap {
|
||||
padding: 0 12px 8px;
|
||||
flex-shrink: 0;
|
||||
@@ -2290,6 +2308,12 @@ select {
|
||||
.help-section.help-hidden {
|
||||
display: none;
|
||||
}
|
||||
.help-search-highlight {
|
||||
background: rgba(250, 204, 21, 0.35);
|
||||
color: inherit;
|
||||
border-radius: 2px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
.help-content {
|
||||
flex: 1;
|
||||
@@ -6393,61 +6417,71 @@ body.popup-mode .content-area {
|
||||
|
||||
/* ── Help Modal: Mobile responsive ── */
|
||||
@media (max-width: 768px) {
|
||||
.help-container { max-width: 100%; height: 100vh; }
|
||||
.help-container .editor-container {
|
||||
.help-container {
|
||||
max-width: 100%;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Hamburger visible on mobile */
|
||||
.help-hamburger {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* TOC: slide-over from left */
|
||||
.help-nav-overlay {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1100;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.help-nav-overlay.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.help-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
max-height: 100vh;
|
||||
height: 100vh;
|
||||
z-index: 1200;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.25s ease;
|
||||
border-right: 1px solid var(--border);
|
||||
box-shadow: 2px 0 16px rgba(0,0,0,0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.help-nav.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.help-nav-close {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* Content full-width */
|
||||
.help-body {
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.help-nav {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-height: none;
|
||||
height: auto;
|
||||
position: relative;
|
||||
top: auto;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
transition: max-height 0.25s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.help-nav-header {
|
||||
padding: 10px 16px 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.help-nav-toggle {
|
||||
display: inline-flex;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.help-nav:not(.collapsed) .help-nav-toggle {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.help-nav.collapsed .help-nav-search-wrap,
|
||||
.help-nav.collapsed .help-nav-list {
|
||||
display: none;
|
||||
}
|
||||
.help-nav-search-wrap {
|
||||
padding: 0 12px 6px;
|
||||
}
|
||||
.help-nav-list {
|
||||
max-height: 45vh;
|
||||
flex: none;
|
||||
padding-bottom: 14px;
|
||||
padding: 0;
|
||||
}
|
||||
.help-content {
|
||||
padding: 14px 12px 40px;
|
||||
padding: 14px 16px 40px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.help-content:focus {
|
||||
outline: none;
|
||||
}
|
||||
.help-content:focus { outline: none; }
|
||||
.help-hero {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
Reference in New Issue
Block a user