Skip to content

Commit 3faa79b

Browse files
authored
Add Collapsible/Expandable Side Nav (TOC) (#707)
1 parent ef2a385 commit 3faa79b

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed

toc.css

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* General TOC container */
2+
#table-of-contents-content {
3+
padding-left: 0;
4+
}
5+
6+
/* Each TOC item */
7+
#table-of-contents-content .toc-item {
8+
list-style: none;
9+
padding: 2px 0;
10+
}
11+
12+
/* Flex container for anchor and toggle */
13+
#table-of-contents-content .toc-row {
14+
display: flex;
15+
align-items: center;
16+
justify-content: space-between;
17+
width: 100%;
18+
}
19+
20+
/* Anchor link styling */
21+
#table-of-contents-content .toc-row a {
22+
flex-grow: 1; /* Allow anchor to take available space */
23+
text-decoration: none;
24+
color: inherit;
25+
word-wrap: break-word;
26+
white-space: normal;
27+
}
28+
29+
/* Toggle button styling */
30+
.toc-toggle {
31+
background: transparent;
32+
border: none;
33+
cursor: pointer;
34+
color: inherit;
35+
padding: 0 0.5rem;
36+
display: inline-flex;
37+
align-items: center;
38+
justify-content: center;
39+
flex-shrink: 0; /* Prevent toggle from shrinking */
40+
}
41+
42+
.toc-toggle:focus {
43+
outline: none;
44+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4);
45+
border-radius: 4px;
46+
}
47+
48+
/* Chevron icon transition */
49+
.toc-toggle svg {
50+
transition: transform 150ms ease-in-out;
51+
}
52+
53+
/* Rotated state for chevron when collapsed */
54+
.toc-toggle.rotated svg {
55+
transform: rotate(-90deg);
56+
}

toc.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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

Comments
 (0)