Skip to content

Commit 5282083

Browse files
committed
Fix heading nav with folded chapters
This fixes an issue when folding is enabled. The folding was not properly hiding the sub-chapters because it was assuming it could hide the next list element. However, the heading nav was the next list element, so the remaining chapters remained visible. The solution required some deeper changes to how the chapters were organized in the sidebar. Instead of nested chapters being a list element *sibling*, the nested chapter's `ol` is now a *child* of its parent chapter. This makes it much easier to just hide everything without regard of the exact sibling order. This required wrapping the chapter title and the toggle chevron inside a span so that the flex layout could be localized to just those elements, and allow the following `ol` elements to lay out regularly. Closes #2880
1 parent 816913b commit 5282083

File tree

20 files changed

+211
-126
lines changed

20 files changed

+211
-126
lines changed

crates/mdbook-html/front-end/css/chrome.css

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -571,17 +571,18 @@ html:not(.sidebar-resizing) .sidebar {
571571
line-height: 2.2em;
572572
}
573573

574-
.chapter ol {
575-
width: 100%;
574+
.chapter li {
575+
color: var(--sidebar-non-existant);
576576
}
577577

578-
.chapter li {
578+
/* This is a span wrapping the chapter link and the fold chevron. */
579+
.chapter-link-wrapper {
580+
/* Used to position the chevron to the right, allowing the text to wrap before it. */
579581
display: flex;
580-
color: var(--sidebar-non-existant);
581582
}
583+
582584
.chapter li a {
583-
display: block;
584-
padding: 0;
585+
/* Remove underlines. */
585586
text-decoration: none;
586587
color: var(--sidebar-fg);
587588
}
@@ -594,21 +595,22 @@ html:not(.sidebar-resizing) .sidebar {
594595
color: var(--sidebar-active);
595596
}
596597

597-
.chapter li > a.toggle {
598+
/* This is the toggle chevron. */
599+
.chapter-fold-toggle {
598600
cursor: pointer;
599-
display: block;
601+
/* Positions the chevron to the side. */
600602
margin-inline-start: auto;
601603
padding: 0 10px;
602604
user-select: none;
603605
opacity: 0.68;
604606
}
605607

606-
.chapter li > a.toggle div {
608+
.chapter-fold-toggle div {
607609
transition: transform 0.5s;
608610
}
609611

610612
/* collapse the section */
611-
.chapter li:not(.expanded) + li > ol {
613+
.chapter li:not(.expanded) > ol {
612614
display: none;
613615
}
614616

@@ -617,10 +619,12 @@ html:not(.sidebar-resizing) .sidebar {
617619
margin-block-start: 0.6em;
618620
}
619621

620-
.chapter li.expanded > a.toggle div {
622+
/* When expanded, rotate the chevron to point down. */
623+
.chapter li.expanded > span > .chapter-fold-toggle div {
621624
transform: rotate(90deg);
622625
}
623626

627+
/* Horizontal line in chapter list. */
624628
.spacer {
625629
width: 100%;
626630
height: 3px;
@@ -630,6 +634,7 @@ html:not(.sidebar-resizing) .sidebar {
630634
background-color: var(--sidebar-spacer);
631635
}
632636

637+
/* On touch devices, add more vertical spacing to make it easier to tap links. */
633638
@media (-moz-touch-enabled: 1), (pointer: coarse) {
634639
.chapter li a { padding: 5px 0; }
635640
.spacer { margin: 10px 0; }
@@ -741,7 +746,6 @@ html:not(.sidebar-resizing) .sidebar {
741746
content: '';
742747
position: absolute;
743748
left: -16px;
744-
top: 0;
745749
margin-top: 10px;
746750
width: 8px;
747751
height: 8px;

crates/mdbook-html/front-end/templates/toc.js.hbs

Lines changed: 62 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,9 @@ class MDBookSidebarScrollbox extends HTMLElement {
2929
&& current_page.endsWith('/index.html')) {
3030
link.classList.add('active');
3131
let parent = link.parentElement;
32-
if (parent && parent.classList.contains('chapter-item')) {
33-
parent.classList.add('expanded');
34-
}
3532
while (parent) {
36-
if (parent.tagName === 'LI' && parent.previousElementSibling) {
37-
if (parent.previousElementSibling.classList.contains('chapter-item')) {
38-
parent.previousElementSibling.classList.add('expanded');
39-
}
33+
if (parent.tagName === 'LI' && parent.classList.contains('chapter-item')) {
34+
parent.classList.add('expanded');
4035
}
4136
parent = parent.parentElement;
4237
}
@@ -62,9 +57,9 @@ class MDBookSidebarScrollbox extends HTMLElement {
6257
}
6358
}
6459
// Toggle buttons
65-
const sidebarAnchorToggles = document.querySelectorAll('#mdbook-sidebar a.toggle');
60+
const sidebarAnchorToggles = document.querySelectorAll('.chapter-fold-toggle');
6661
function toggleSection(ev) {
67-
ev.currentTarget.parentElement.classList.toggle('expanded');
62+
ev.currentTarget.parentElement.parentElement.classList.toggle('expanded');
6863
}
6964
Array.from(sidebarAnchorToggles).forEach(el => {
7065
el.addEventListener('click', toggleSection);
@@ -237,17 +232,12 @@ window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox)
237232
// be expanded.
238233
function updateHeaderExpanded(currentA) {
239234
// Add expanded to all header-item li ancestors.
240-
let current = currentA.parentElement.parentElement.parentElement;
241-
while (current.tagName === 'LI') {
242-
const prevSibling = current.previousElementSibling;
243-
if (prevSibling !== null
244-
&& prevSibling.tagName === 'LI'
245-
&& prevSibling.classList.contains('header-item')) {
246-
prevSibling.classList.add('expanded');
247-
current = prevSibling.parentElement.parentElement;
248-
} else {
249-
break;
235+
let current = currentA.parentElement;
236+
while (current) {
237+
if (current.tagName === 'LI' && current.classList.contains('header-item')) {
238+
current.classList.add('expanded');
250239
}
240+
current = current.parentElement;
251241
}
252242
}
253243
@@ -343,19 +333,6 @@ window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox)
343333
if (activeSection === null) {
344334
return;
345335
}
346-
const activeItem = activeSection.parentElement;
347-
const activeList = activeItem.parentElement;
348-
349-
// Build a tree of headers in the sidebar.
350-
const rootLi = document.createElement('li');
351-
rootLi.classList.add('header-item');
352-
rootLi.classList.add('expanded');
353-
const rootOl = document.createElement('ol');
354-
rootOl.classList.add('section');
355-
rootLi.appendChild(rootOl);
356-
const stack = [{ level: 0, ol: rootOl }];
357-
// The level where it will start folding deeply nested headers.
358-
const foldLevel = 3;
359336
360337
const main = document.getElementsByTagName('main')[0];
361338
headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6'))
@@ -365,57 +342,90 @@ window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox)
365342
return;
366343
}
367344
345+
// Build a tree of headers in the sidebar.
346+
347+
const stack = [];
348+
349+
const firstLevel = parseInt(headers[0].tagName.charAt(1));
350+
for (let i = 1; i < firstLevel; i++) {
351+
const ol = document.createElement('ol');
352+
ol.classList.add('section');
353+
if (stack.length > 0) {
354+
stack[stack.length - 1].ol.appendChild(ol);
355+
}
356+
stack.push({level: i + 1, ol: ol});
357+
}
358+
359+
// The level where it will start folding deeply nested headers.
360+
const foldLevel = 3;
361+
368362
for (let i = 0; i < headers.length; i++) {
369363
const header = headers[i];
370364
const level = parseInt(header.tagName.charAt(1));
365+
366+
const currentLevel = stack[stack.length - 1].level;
367+
if (level > currentLevel) {
368+
// Begin nesting to this level.
369+
for (let nextLevel = currentLevel + 1; nextLevel <= level; nextLevel++) {
370+
const ol = document.createElement('ol');
371+
ol.classList.add('section');
372+
const last = stack[stack.length - 1];
373+
const lastChild = last.ol.lastChild;
374+
// Handle the case where jumping more than one nesting
375+
// level, which doesn't have a list item to place this new
376+
// list inside of.
377+
if (lastChild) {
378+
lastChild.appendChild(ol);
379+
} else {
380+
last.ol.appendChild(ol);
381+
}
382+
stack.push({level: nextLevel, ol: ol});
383+
}
384+
} else if (level < currentLevel) {
385+
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
386+
stack.pop();
387+
}
388+
}
389+
371390
const li = document.createElement('li');
372391
li.classList.add('header-item');
373392
li.classList.add('expanded');
374393
if (level < foldLevel) {
375394
li.classList.add('expanded');
376395
}
396+
const span = document.createElement('span');
397+
span.classList.add('chapter-link-wrapper');
377398
const a = document.createElement('a');
399+
span.appendChild(a);
378400
a.href = '#' + header.id;
379401
a.classList.add('header-in-summary');
380402
a.innerHTML = header.children[0].innerHTML;
381403
a.addEventListener('click', headerThresholdClick);
382-
li.appendChild(a);
383404
const nextHeader = headers[i + 1];
384405
if (nextHeader !== undefined) {
385406
const nextLevel = parseInt(nextHeader.tagName.charAt(1));
386407
if (nextLevel > level && level >= foldLevel) {
387-
const div = document.createElement('div');
388-
div.textContent = '❱';
389408
const toggle = document.createElement('a');
390-
toggle.classList.add('toggle');
409+
toggle.classList.add('chapter-fold-toggle');
391410
toggle.classList.add('header-toggle');
392-
toggle.appendChild(div);
393411
toggle.addEventListener('click', () => {
394412
li.classList.toggle('expanded');
395413
});
396-
li.appendChild(toggle);
414+
const toggleDiv = document.createElement('div');
415+
toggleDiv.textContent = '❱';
416+
toggle.appendChild(toggleDiv);
417+
span.appendChild(toggle);
397418
headerToggles.push(li);
398419
}
399420
}
400-
401-
// Find the appropriate parent level.
402-
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
403-
stack.pop();
404-
}
421+
li.appendChild(span);
405422
406423
const currentParent = stack[stack.length - 1];
407424
currentParent.ol.appendChild(li);
408-
409-
// Create new nested ol for potential children.
410-
const nestedOl = document.createElement('ol');
411-
nestedOl.classList.add('section');
412-
const nestedLi = document.createElement('li');
413-
nestedLi.appendChild(nestedOl);
414-
currentParent.ol.appendChild(nestedLi);
415-
stack.push({ level: level, ol: nestedOl });
416425
}
417426
418-
activeList.insertBefore(rootLi, activeItem.nextSibling);
427+
const activeItemSpan = activeSection.parentElement;
428+
activeItemSpan.after(stack[0].ol);
419429
});
420430
421431
document.addEventListener('DOMContentLoaded', reloadCurrentHeader);

crates/mdbook-html/src/html_handlebars/helpers/toc.rs

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -57,39 +57,45 @@ impl HelperDef for RenderToc {
5757
out.write("<ol class=\"chapter\">")?;
5858

5959
let mut current_level = 1;
60+
let mut first = true;
6061

6162
for item in chapters {
62-
let (_section, level) = if let Some(s) = item.get("section") {
63-
(s.as_str(), s.matches('.').count())
64-
} else {
65-
("", 1)
66-
};
63+
let level = item
64+
.get("section")
65+
.map(|s| s.matches('.').count())
66+
.unwrap_or(1);
6767

6868
// Expand if folding is disabled, or if levels that are larger than this would not
6969
// be folded.
7070
let is_expanded = !fold_enable || level - 1 < (fold_level as usize);
7171

7272
match level.cmp(&current_level) {
7373
Ordering::Greater => {
74-
while level > current_level {
75-
out.write("<li>")?;
76-
out.write("<ol class=\"section\">")?;
77-
current_level += 1;
78-
}
79-
write_li_open_tag(out, is_expanded, false)?;
74+
// There is an assumption that when descending, it can
75+
// only go one level down at a time. This should be
76+
// enforced by the nature of markdown lists and the
77+
// summary parser.
78+
assert_eq!(level, current_level + 1);
79+
current_level += 1;
80+
out.write("<ol class=\"section\">")?;
81+
write_li_open_tag(out, is_expanded)?;
8082
}
8183
Ordering::Less => {
8284
while level < current_level {
83-
out.write("</ol>")?;
8485
out.write("</li>")?;
86+
out.write("</ol>")?;
8587
current_level -= 1;
8688
}
87-
write_li_open_tag(out, is_expanded, false)?;
89+
write_li_open_tag(out, is_expanded)?;
8890
}
8991
Ordering::Equal => {
90-
write_li_open_tag(out, is_expanded, !item.contains_key("section"))?;
92+
if !first {
93+
out.write("</li>")?;
94+
}
95+
write_li_open_tag(out, is_expanded)?;
9196
}
9297
}
98+
first = false;
9399

94100
// Spacer
95101
if item.contains_key("spacer") {
@@ -105,6 +111,8 @@ impl HelperDef for RenderToc {
105111
continue;
106112
}
107113

114+
out.write("<span class=\"chapter-link-wrapper\">")?;
115+
108116
// Link
109117
let path_exists = match item.get("path") {
110118
Some(path) if !path.is_empty() => {
@@ -121,7 +129,7 @@ impl HelperDef for RenderToc {
121129
true
122130
}
123131
_ => {
124-
out.write("<div>")?;
132+
out.write("<span>")?;
125133
false
126134
}
127135
};
@@ -142,41 +150,35 @@ impl HelperDef for RenderToc {
142150
if path_exists {
143151
out.write("</a>")?;
144152
} else {
145-
out.write("</div>")?;
153+
out.write("</span>")?;
146154
}
147155

148156
// Render expand/collapse toggle
149157
if let Some(flag) = item.get("has_sub_items") {
150158
let has_sub_items = flag.parse::<bool>().unwrap_or_default();
151159
if fold_enable && has_sub_items {
152-
out.write("<a class=\"toggle\"><div>❱</div></a>")?;
160+
// The <div> here is to manage rotating the element when
161+
// the chapter title is long and word-wraps.
162+
out.write("<a class=\"chapter-fold-toggle\"><div>❱</div></a>")?;
153163
}
154164
}
155-
out.write("</li>")?;
165+
out.write("</span>")?;
156166
}
157-
while current_level > 1 {
158-
out.write("</ol>")?;
167+
while current_level > 0 {
159168
out.write("</li>")?;
169+
out.write("</ol>")?;
160170
current_level -= 1;
161171
}
162172

163-
out.write("</ol>")?;
164173
Ok(())
165174
}
166175
}
167176

168-
fn write_li_open_tag(
169-
out: &mut dyn Output,
170-
is_expanded: bool,
171-
is_affix: bool,
172-
) -> Result<(), std::io::Error> {
177+
fn write_li_open_tag(out: &mut dyn Output, is_expanded: bool) -> Result<(), std::io::Error> {
173178
let mut li = String::from("<li class=\"chapter-item ");
174179
if is_expanded {
175180
li.push_str("expanded ");
176181
}
177-
if is_affix {
178-
li.push_str("affix ");
179-
}
180182
li.push_str("\">");
181183
out.write(&li)
182184
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[book]
2+
title = "heading-nav-folded"
3+
4+
[output.html.fold]
5+
enable = true
6+
level = 0

0 commit comments

Comments
 (0)