Skip to content

Commit 7b7dee4

Browse files
authored
Merge pull request #2893 from ehuss/fix-fold-sub-chapter
Fix heading nav with folded chapters
2 parents 816913b + 5282083 commit 7b7dee4

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)