|
| 1 | +// This script enhances the Table of Contents (TOC) / right side navigation of Mintliffy by adding collapsible/expandable sections based on header depth. |
| 2 | +// It also fixes issues with incorrect depth values for custom anchor tags and ensures proper indentation and visibility of TOC items. |
| 3 | +// But custom anchor tags are not used anymore in our repository. So that part is not needed anymore. |
| 4 | + |
| 5 | +// toc-fix-v2.js |
| 6 | +(function () { |
| 7 | + let observer; |
| 8 | + |
| 9 | + function run() { |
| 10 | + const toc = document.getElementById('table-of-contents-content'); |
| 11 | + if (!toc || toc.dataset.enhanced === 'true') { |
| 12 | + return; |
| 13 | + } |
| 14 | + toc.dataset.enhanced = 'true'; |
| 15 | + |
| 16 | + // Disconnect the observer while we modify the DOM to prevent infinite loops |
| 17 | + if (observer) observer.disconnect(); |
| 18 | + |
| 19 | + const items = Array.from(toc.querySelectorAll('.toc-item')); |
| 20 | + |
| 21 | + // Normalize depths, fix NaN, and set initial styles |
| 22 | + items.forEach(li => { |
| 23 | + let depthNum = Number(li.getAttribute('data-depth')); |
| 24 | + if (!Number.isFinite(depthNum)) { |
| 25 | + depthNum = 2; // Fix for h4 headers producing NaN |
| 26 | + } |
| 27 | + li.dataset.depth = String(depthNum); |
| 28 | + li.style.display = 'list-item'; // Ensure all items are visible initially |
| 29 | + |
| 30 | + const a = li.querySelector('a'); |
| 31 | + if (a) { |
| 32 | + const indentStep = 1; // rem per depth level |
| 33 | + a.style.marginLeft = `${depthNum * indentStep}rem`; |
| 34 | + } |
| 35 | + }); |
| 36 | + |
| 37 | + // Add toggle buttons and functionality |
| 38 | + items.forEach((li, idx) => { |
| 39 | + const depth = Number(li.dataset.depth); |
| 40 | + |
| 41 | + // Detect if this item has children |
| 42 | + const hasChild = idx + 1 < items.length && Number(items[idx + 1].dataset.depth) > depth; |
| 43 | + if (!hasChild) return; |
| 44 | + |
| 45 | + // Prevent adding duplicate toggles if script re-runs |
| 46 | + if (li.querySelector('.toc-toggle')) return; |
| 47 | + |
| 48 | + const toggle = document.createElement('button'); |
| 49 | + toggle.type = 'button'; |
| 50 | + toggle.className = 'toc-toggle'; |
| 51 | + toggle.setAttribute('aria-expanded', 'true'); |
| 52 | + toggle.innerHTML = ` |
| 53 | + <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true" focusable="false"> |
| 54 | + <path d="M8.5 10.5 L12 14 L15.5 10.5" stroke="currentColor" stroke-width="1.6" fill="none" |
| 55 | + stroke-linecap="round" stroke-linejoin="round"/> |
| 56 | + </svg> |
| 57 | + `; |
| 58 | + |
| 59 | + const anchor = li.querySelector('a'); |
| 60 | + if (anchor) { |
| 61 | + const wrapper = document.createElement('div'); |
| 62 | + wrapper.className = 'toc-row'; |
| 63 | + // Insert wrapper before the anchor, then move anchor and toggle into it |
| 64 | + anchor.parentNode.insertBefore(wrapper, anchor); |
| 65 | + wrapper.appendChild(anchor); |
| 66 | + wrapper.appendChild(toggle); |
| 67 | + } else { |
| 68 | + li.appendChild(toggle); |
| 69 | + } |
| 70 | + |
| 71 | + li.dataset.collapsed = 'false'; |
| 72 | + |
| 73 | + toggle.addEventListener('click', e => { |
| 74 | + e.preventDefault(); |
| 75 | + const isCollapsed = li.dataset.collapsed === 'true'; |
| 76 | + |
| 77 | + // Toggle the state |
| 78 | + li.dataset.collapsed = String(!isCollapsed); |
| 79 | + toggle.setAttribute('aria-expanded', String(!isCollapsed)); |
| 80 | + toggle.classList.toggle('rotated', !isCollapsed); |
| 81 | + |
| 82 | + // Update visibility of all descendant items |
| 83 | + for (let k = idx + 1; k < items.length; k++) { |
| 84 | + const childItem = items[k]; |
| 85 | + const childDepth = Number(childItem.dataset.depth); |
| 86 | + |
| 87 | + if (childDepth <= depth) { |
| 88 | + break; // Exited the subtree of the clicked item |
| 89 | + } |
| 90 | + |
| 91 | + if (!isCollapsed) { |
| 92 | + // If we are COLLAPSING, hide all descendants |
| 93 | + childItem.style.display = 'none'; |
| 94 | + } else { |
| 95 | + // If we are EXPANDING, only reveal direct children |
| 96 | + // Deeper children remain hidden if their own parent is collapsed |
| 97 | + if (childDepth === depth + 1) { |
| 98 | + childItem.style.display = 'list-item'; |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + }); |
| 103 | + }); |
| 104 | + |
| 105 | + // Reconnect the observer after DOM modifications are complete |
| 106 | + if (observer) { |
| 107 | + observer.observe(document.body, { childList: true, subtree: true }); |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + function init() { |
| 112 | + run(); // Run on initial load |
| 113 | + |
| 114 | + // Set up an observer to re-run the script on SPA navigation |
| 115 | + observer = new MutationObserver((mutations) => { |
| 116 | + for (const mutation of mutations) { |
| 117 | + if (mutation.addedNodes.length > 0) { |
| 118 | + const toc = document.getElementById('table-of-contents-content'); |
| 119 | + // If a TOC exists and it hasn't been enhanced yet, run the script |
| 120 | + if (toc && !toc.dataset.enhanced) { |
| 121 | + run(); |
| 122 | + break; // Found and processed a new TOC, no need to check other mutations |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + }); |
| 127 | + observer.observe(document.body, { childList: true, subtree: true }); |
| 128 | + } |
| 129 | + |
| 130 | + if (document.readyState === 'loading') { |
| 131 | + document.addEventListener('DOMContentLoaded', init); |
| 132 | + } else { |
| 133 | + init(); |
| 134 | + } |
| 135 | +})(); |
0 commit comments