Docs: Improved the navigation seperating out ToC from top nac bar
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
* - When the reader scrolls past a section, any items that were only
|
||||
* auto-expanded (not user-pinned) collapse back down.
|
||||
* - Works with mkdocs-material instant-loading / SPA navigation.
|
||||
*
|
||||
* Page navigation is handled by the top navbar tabs + dropdowns.
|
||||
* This script only manages the TOC (md-nav--secondary) in the left sidebar.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
@@ -68,11 +71,9 @@
|
||||
//
|
||||
// Scans `containerEl` for <li> items with nested <nav> children and
|
||||
// turns them into collapsible items: adds the --nested class, collapses
|
||||
// them, and injects a toggle chevron. This is used both for the active
|
||||
// page's built-in TOC (md-nav--secondary) and for TOC items lazily
|
||||
// injected into converted single-page nav sections.
|
||||
// them, and injects a toggle chevron.
|
||||
|
||||
function makeItemsCollapsible(containerEl, trackPinning) {
|
||||
function makeItemsCollapsible(containerEl) {
|
||||
containerEl.querySelectorAll("li.md-nav__item").forEach(function (li) {
|
||||
var nestedNav = li.querySelector(":scope > nav.md-nav, :scope > .md-nav");
|
||||
if (!nestedNav) return;
|
||||
@@ -101,10 +102,10 @@
|
||||
var isCollapsed = li.classList.contains("toc-collapsed");
|
||||
if (isCollapsed) {
|
||||
expand(li);
|
||||
if (trackPinning) userPinned.add(li);
|
||||
userPinned.add(li);
|
||||
} else {
|
||||
collapse(li);
|
||||
if (trackPinning) userPinned.delete(li);
|
||||
userPinned.delete(li);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -121,25 +122,27 @@
|
||||
userPinned.clear();
|
||||
autoExpanded.clear();
|
||||
|
||||
// Force the TOC visible. Material's base CSS hides the secondary
|
||||
// nav via a sibling-combinator rule keyed off the #__toc checkbox.
|
||||
// We force-check it and set inline display so the TOC is guaranteed
|
||||
// visible regardless of the CSS cascade.
|
||||
var tocCheckbox = sidebar.querySelector("#__toc");
|
||||
if (tocCheckbox) {
|
||||
tocCheckbox.checked = true;
|
||||
}
|
||||
|
||||
// Find the actual TOC (md-nav--secondary). We scope to the secondary
|
||||
// nav to avoid touching the page-level nav items that wrap sections —
|
||||
// those use Material's native checkbox-based toggle and should not be
|
||||
// overridden.
|
||||
//
|
||||
// NOTE: The primary nav already has .md-nav--integrated added by
|
||||
// mkdocs-material when the toc.integrate feature is enabled. We do
|
||||
// NOT re-add that class or extend it to descendant navs — doing so
|
||||
// causes the CSS rule ".md-nav--integrated .md-nav__item--nested >
|
||||
// .md-nav { display: block }" to override Material's native
|
||||
// checkbox-based collapse on primary nav sections (Reference,
|
||||
// Development, Architecture Decisions, etc.), creating whitespace
|
||||
// below collapsed section headings.
|
||||
// nav to avoid touching the page-level nav items that wrap sections.
|
||||
var tocNav = sidebar.querySelector("nav.md-nav--secondary");
|
||||
if (!tocNav) return;
|
||||
|
||||
tocNav.style.display = "block";
|
||||
tocNav.style.opacity = "1";
|
||||
tocNav.style.visibility = "visible";
|
||||
|
||||
// Make all nested TOC items collapsible, with pinning support for
|
||||
// the active-link watcher.
|
||||
makeItemsCollapsible(tocNav, true);
|
||||
makeItemsCollapsible(tocNav);
|
||||
|
||||
// Kick off the active-link watcher
|
||||
watchActiveLink();
|
||||
@@ -229,336 +232,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── Convert leaf-page nav items into section-like structures ─────────
|
||||
//
|
||||
// Material renders leaf-page nav items (any <li> with just an <a> link
|
||||
// and no nested <nav>) as flat links with no expand/collapse chevron.
|
||||
// Only native sections (Reference, Development, Architecture Decisions)
|
||||
// get a checkbox + label + nested <nav>.
|
||||
//
|
||||
// This function converts leaf-page items at two levels:
|
||||
// 1. Top-level items (Specification, Work Remaining, FAQ)
|
||||
// 2. Children inside native sections (Action CLI, Session Model, etc.)
|
||||
//
|
||||
// It explicitly avoids touching items inside md-nav--secondary (the TOC)
|
||||
// or md-nav--page-toc (our own lazy-loaded TOC containers).
|
||||
//
|
||||
// The ACTIVE page (wherever it sits in the tree) is treated specially:
|
||||
// it becomes an always-expanded, non-collapsible section with a
|
||||
// <label> heading instead of an <a>, matching how Material handles
|
||||
// native sections that contain the current page.
|
||||
|
||||
/** Counter for unique checkbox IDs across conversions. */
|
||||
var pageNavCounter = 100;
|
||||
|
||||
/** Cache of fetched TOC HTML keyed by page URL. */
|
||||
var tocCache = {};
|
||||
|
||||
/**
|
||||
* Collect the <ul> lists whose direct <li> children should be
|
||||
* converted. This includes:
|
||||
* - The top-level nav list (direct child of md-nav--primary)
|
||||
* - The child lists inside native sections (level-1 <nav> elements
|
||||
* that are NOT md-nav--secondary and NOT md-nav--page-toc)
|
||||
*/
|
||||
function collectConvertibleLists(primaryNav) {
|
||||
var lists = [];
|
||||
|
||||
// 1. Top-level list
|
||||
var topList = primaryNav.querySelector(":scope > .md-nav__list");
|
||||
if (topList) lists.push(topList);
|
||||
|
||||
// 2. Lists inside native sections (one level deep)
|
||||
if (topList) {
|
||||
topList.querySelectorAll(":scope > li.md-nav__item--nested").forEach(function (sectionLi) {
|
||||
var sectionNav = sectionLi.querySelector(":scope > nav.md-nav:not(.md-nav--secondary):not(.md-nav--page-toc)");
|
||||
if (sectionNav) {
|
||||
var sectionList = sectionNav.querySelector(":scope > .md-nav__list");
|
||||
if (sectionList) lists.push(sectionList);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert leaf-page <li> items into section-like expandable structures.
|
||||
* Only operates on items in the top-level nav and inside native sections.
|
||||
*/
|
||||
function convertPageItems() {
|
||||
var primaryNav = document.querySelector("nav.md-nav--primary");
|
||||
if (!primaryNav) return;
|
||||
|
||||
var lists = collectConvertibleLists(primaryNav);
|
||||
|
||||
lists.forEach(function (ul) {
|
||||
// Only process DIRECT children of each list
|
||||
var children = ul.querySelectorAll(":scope > li.md-nav__item");
|
||||
children.forEach(function (li) {
|
||||
// Skip items already converted or that are native sections
|
||||
if (li.classList.contains("md-nav__item--nested")) return;
|
||||
|
||||
// Active page items need special handling — always-expanded,
|
||||
// non-collapsible, matching native section behaviour.
|
||||
if (li.classList.contains("md-nav__item--active")) {
|
||||
convertActivePageItem(li);
|
||||
return;
|
||||
}
|
||||
|
||||
convertInactivePageItem(li);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an INACTIVE leaf-page <li> into a section-like structure
|
||||
* with a checkbox + label + nested nav that lazy-loads the page's TOC.
|
||||
*/
|
||||
function convertInactivePageItem(li) {
|
||||
var link = li.querySelector(":scope > a.md-nav__link");
|
||||
if (!link) return;
|
||||
|
||||
var href = link.getAttribute("href");
|
||||
var text = link.textContent.trim();
|
||||
var navId = "__nav_" + (pageNavCounter++);
|
||||
|
||||
// Build the checkbox
|
||||
var checkbox = document.createElement("input");
|
||||
checkbox.className = "md-nav__toggle md-toggle";
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.id = navId;
|
||||
|
||||
// Build the label (with chevron icon) — identical to native sections
|
||||
var label = document.createElement("label");
|
||||
label.className = "md-nav__link";
|
||||
label.setAttribute("for", navId);
|
||||
label.tabIndex = 0;
|
||||
|
||||
var ellipsis = document.createElement("span");
|
||||
ellipsis.className = "md-ellipsis";
|
||||
ellipsis.textContent = text;
|
||||
label.appendChild(ellipsis);
|
||||
|
||||
var icon = document.createElement("span");
|
||||
icon.className = "md-nav__icon md-icon";
|
||||
label.appendChild(icon);
|
||||
|
||||
// Build the nested nav with the original link as first child
|
||||
var nav = document.createElement("nav");
|
||||
nav.className = "md-nav md-nav--page-toc";
|
||||
nav.setAttribute("data-md-level", "1");
|
||||
nav.setAttribute("aria-expanded", "false");
|
||||
|
||||
var ul = document.createElement("ul");
|
||||
ul.className = "md-nav__list";
|
||||
|
||||
// First child: a link to the page itself (like section index pages)
|
||||
var indexLi = document.createElement("li");
|
||||
indexLi.className = "md-nav__item";
|
||||
var indexLink = document.createElement("a");
|
||||
indexLink.className = "md-nav__link";
|
||||
indexLink.href = href;
|
||||
var indexSpan = document.createElement("span");
|
||||
indexSpan.className = "md-ellipsis";
|
||||
indexSpan.textContent = text;
|
||||
indexLink.appendChild(indexSpan);
|
||||
indexLi.appendChild(indexLink);
|
||||
ul.appendChild(indexLi);
|
||||
|
||||
nav.appendChild(ul);
|
||||
|
||||
// Replace the <li> contents
|
||||
li.innerHTML = "";
|
||||
li.classList.add("md-nav__item--nested");
|
||||
li.appendChild(checkbox);
|
||||
li.appendChild(label);
|
||||
li.appendChild(nav);
|
||||
|
||||
// Lazy-load the TOC when first expanded
|
||||
checkbox.addEventListener("change", function () {
|
||||
if (!checkbox.checked) return;
|
||||
if (tocCache[href]) {
|
||||
injectToc(ul, tocCache[href]);
|
||||
return;
|
||||
}
|
||||
loadToc(href, ul);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ACTIVE page item to look like a native expanded section.
|
||||
*
|
||||
* Material emits a __toc checkbox + label (with a TOC-list icon) plus
|
||||
* an <a> link and the md-nav--secondary TOC nav. We hide the __toc
|
||||
* machinery and replace the <a> with a non-navigating <label> that
|
||||
* has the same section-heading + chevron appearance as native sections.
|
||||
*
|
||||
* The TOC stays always-visible and cannot be collapsed — matching how
|
||||
* Material treats native sections that contain the current page.
|
||||
*/
|
||||
function convertActivePageItem(li) {
|
||||
// 1. Force-check and hide the __toc checkbox so Material keeps the
|
||||
// TOC visible, and prevent any future toggling.
|
||||
var tocCheckbox = li.querySelector(":scope > #__toc");
|
||||
if (tocCheckbox) {
|
||||
tocCheckbox.checked = true;
|
||||
tocCheckbox.disabled = true;
|
||||
tocCheckbox.style.display = "none";
|
||||
}
|
||||
|
||||
// 2. Hide the __toc label (it has the wrong icon and toggles the
|
||||
// checkbox — we don't want either).
|
||||
var tocLabel = li.querySelector(':scope > label[for="__toc"]');
|
||||
if (tocLabel) {
|
||||
tocLabel.style.display = "none";
|
||||
}
|
||||
|
||||
// 3. Mark the <li> as nested + our custom marker so CSS can style it.
|
||||
li.classList.add("md-nav__item--nested");
|
||||
li.classList.add("md-nav__item--section-page");
|
||||
|
||||
// 4. Replace the <a> with a non-navigating <label> that matches the
|
||||
// exact structure of native section headings.
|
||||
var link = li.querySelector(":scope > a.md-nav__link");
|
||||
if (link) {
|
||||
var text = link.textContent.trim();
|
||||
|
||||
var heading = document.createElement("label");
|
||||
heading.className = "md-nav__link";
|
||||
heading.tabIndex = 0;
|
||||
|
||||
var ellipsis = document.createElement("span");
|
||||
ellipsis.className = "md-ellipsis";
|
||||
ellipsis.textContent = text;
|
||||
heading.appendChild(ellipsis);
|
||||
|
||||
var icon = document.createElement("span");
|
||||
icon.className = "md-nav__icon md-icon";
|
||||
heading.appendChild(icon);
|
||||
|
||||
link.parentNode.replaceChild(heading, link);
|
||||
}
|
||||
|
||||
// 5. Force the TOC nav always visible (belt-and-suspenders alongside
|
||||
// the CSS rule).
|
||||
var tocNav = li.querySelector(":scope > nav.md-nav--secondary");
|
||||
if (tocNav) {
|
||||
tocNav.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a page and extract its TOC <ul> content, then inject it.
|
||||
*/
|
||||
function loadToc(href, targetUl) {
|
||||
// Add a placeholder
|
||||
var placeholder = document.createElement("li");
|
||||
placeholder.className = "md-nav__item md-nav__toc-placeholder";
|
||||
placeholder.textContent = "Loading…";
|
||||
targetUl.appendChild(placeholder);
|
||||
|
||||
fetch(href)
|
||||
.then(function (res) { return res.text(); })
|
||||
.then(function (html) {
|
||||
// Remove placeholder
|
||||
if (placeholder.parentNode) placeholder.parentNode.removeChild(placeholder);
|
||||
|
||||
// Parse the fetched page and extract TOC items
|
||||
var parser = new DOMParser();
|
||||
var doc = parser.parseFromString(html, "text/html");
|
||||
var tocList = doc.querySelector("[data-md-component='toc']");
|
||||
if (!tocList) return;
|
||||
|
||||
// Cache the TOC items (as an array of <li> outerHTML)
|
||||
var items = [];
|
||||
tocList.querySelectorAll(":scope > li").forEach(function (li) {
|
||||
items.push(li.outerHTML);
|
||||
});
|
||||
tocCache[href] = items;
|
||||
injectToc(targetUl, items);
|
||||
})
|
||||
.catch(function () {
|
||||
if (placeholder.parentNode) placeholder.parentNode.removeChild(placeholder);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject cached TOC items into the target <ul>.
|
||||
*/
|
||||
function injectToc(targetUl, items) {
|
||||
// Don't inject if already done
|
||||
if (targetUl.dataset.tocLoaded) return;
|
||||
targetUl.dataset.tocLoaded = "true";
|
||||
|
||||
items.forEach(function (itemHtml) {
|
||||
var temp = document.createElement("template");
|
||||
temp.innerHTML = itemHtml;
|
||||
var li = temp.content.firstElementChild;
|
||||
if (li) {
|
||||
// Fix relative anchor links — they need the page href prefix
|
||||
li.querySelectorAll("a[href^='#']").forEach(function (a) {
|
||||
var pageLink = targetUl.querySelector("li:first-child a");
|
||||
if (pageLink) {
|
||||
a.href = pageLink.href + a.getAttribute("href");
|
||||
}
|
||||
});
|
||||
targetUl.appendChild(li);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialise collapse/expand behaviour on any nested items that
|
||||
// were just injected (sub-sections inside the fetched TOC).
|
||||
// No pinning tracking — these are non-active pages with no
|
||||
// MutationObserver watching them.
|
||||
makeItemsCollapsible(targetUl, false);
|
||||
}
|
||||
|
||||
// ── Lock active ancestor sections ────────────────────────────────────
|
||||
//
|
||||
// When you are viewing a page inside a section (e.g. CI/CD Pipeline
|
||||
// inside Development), Material marks every ancestor section <li> with
|
||||
// md-nav__item--active and checks its toggle. By default the user can
|
||||
// still uncheck (collapse) those sections. We prevent this so that
|
||||
// every section on the path to the active page stays permanently open,
|
||||
// matching the intuitive expectation that you cannot collapse a section
|
||||
// you are currently inside.
|
||||
|
||||
function lockActiveAncestorSections() {
|
||||
var primaryNav = document.querySelector("nav.md-nav--primary");
|
||||
if (!primaryNav) return;
|
||||
|
||||
// Find native sections that are ancestors of the current page.
|
||||
// These have both --active (propagated up from the active page)
|
||||
// and --nested (they are real sections, not converted page items).
|
||||
primaryNav.querySelectorAll(
|
||||
"li.md-nav__item--active.md-nav__item--nested:not(.md-nav__item--section-page)"
|
||||
).forEach(function (li) {
|
||||
var toggle = li.querySelector(":scope > input.md-nav__toggle");
|
||||
if (!toggle) return;
|
||||
|
||||
// Force checked and prevent user from unchecking
|
||||
toggle.checked = true;
|
||||
|
||||
// Intercept click/change to keep it locked
|
||||
toggle.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
toggle.addEventListener("change", function () {
|
||||
toggle.checked = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Desktop detection ────────────────────────────────────────────────
|
||||
//
|
||||
// This entire script is designed for the desktop persistent sidebar
|
||||
// (≥76.25em). On mobile, Material uses a slide-out drawer with its
|
||||
// own drill-down navigation, scroll management, and back-button
|
||||
// behaviour. Running our DOM restructuring (convertPageItems,
|
||||
// initCollapsibleToc, lockActiveAncestorSections) on mobile breaks
|
||||
// the drawer's scroll container and makes it impossible to scroll
|
||||
// past the visible items.
|
||||
// behaviour. Running our DOM restructuring on mobile breaks the
|
||||
// drawer's scroll container and makes it impossible to scroll past
|
||||
// the visible items.
|
||||
//
|
||||
// We use matchMedia to detect the breakpoint and only bootstrap on
|
||||
// desktop. On resize/orientation changes that cross the breakpoint,
|
||||
@@ -576,8 +257,6 @@
|
||||
function bootstrap() {
|
||||
if (!isDesktop()) return; // skip all DOM manipulation on mobile
|
||||
initCollapsibleToc();
|
||||
convertPageItems();
|
||||
lockActiveAncestorSections();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
@@ -589,9 +268,6 @@
|
||||
// Re-init after instant navigation (mkdocs-material SPA mode)
|
||||
if (typeof document$ !== "undefined") {
|
||||
document$.subscribe(function () {
|
||||
// Reset TOC cache and counter on navigation
|
||||
tocCache = {};
|
||||
pageNavCounter = 100;
|
||||
bootstrap();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user