feat(toc): add folding TOC elements for more compact TOC table

This commit is contained in:
David Bailey 2025-02-13 10:45:32 +01:00
parent da25a786d1
commit 9870967093
3 changed files with 77 additions and 14 deletions

View file

@ -165,8 +165,11 @@ a:hover {
margin-right: 2rem; margin-right: 2rem;
} }
:target { :target, html, body {
scroll-margin-top: 6rem; scroll-margin-top: 140px;
}
:target {
outline: 1px solid var(--highlight_1);
} }
#main_content_flexbox { #main_content_flexbox {

View file

@ -37,7 +37,7 @@
color: var(--toc-fg) !important; color: var(--toc-fg) !important;
} }
& .active { & .active > a {
background-color: var(--toc-bg); background-color: var(--toc-bg);
border-bottom: 1px solid var(--highlight_1); border-bottom: 1px solid var(--highlight_1);
@ -57,4 +57,11 @@
margin-bottom: 0.2em; margin-bottom: 0.2em;
transition: all 1s; transition: all 1s;
} }
& .toc_collapsing {
display: none;
}
& li:is(:has(.active),.active) > ol > .toc_collapsing {
display: block;
}
} }

View file

@ -7,7 +7,7 @@ let tocId = "toc";
const TOC_IDENTIFIER = "toc"; const TOC_IDENTIFIER = "toc";
const TOC_MAIN_CONTENT_ID = "#content_article" const TOC_MAIN_CONTENT_ID = "#content_article"
const TOC_TOP_PIXEL_MARGIN = 300; const TOC_TOP_PIXEL_MARGIN = 150;
class TocTracker { class TocTracker {
known_heading_elements = []; known_heading_elements = [];
@ -49,14 +49,25 @@ class TocTracker {
throw Error("A `main` tag section is required to query headings from."); throw Error("A `main` tag section is required to query headings from.");
} }
let headings = main.querySelectorAll("h1, h2, h3, h4, h5, h6"); let headings = main.querySelectorAll("h1, h2, h3, h4, h5, h6, li strong");
headings.forEach((heading, index) => { headings.forEach((heading, index) => {
const heading_level = parseInt(heading.tagName.slice(-1)); let heading_level = parseInt(heading.tagName.slice(-1));
let heading_collapsed = false;
let heading_name = heading.innerHTML;
if(heading.tagName == 'STRONG') {
heading_level = -1;
heading_collapsed = true;
heading = heading.closest('li');
}
this.known_heading_elements.push({ this.known_heading_elements.push({
level: heading_level, level: heading_level,
name: heading.innerHTML, name: heading_name,
dom: heading, dom: heading,
collapse: heading_collapsed
}); });
}); });
} }
@ -65,6 +76,23 @@ class TocTracker {
this.known_heading_elements.sort((a, b) => { this.known_heading_elements.sort((a, b) => {
return a.dom.getBoundingClientRect().y return a.dom.getBoundingClientRect().y
- b.dom.getBoundingClientRect().y; - b.dom.getBoundingClientRect().y;
});
let lastHeadingLevel = 0;
this.known_heading_elements.forEach((element) => {
if(element.level == -1) {
let extra_depth = 0;
let current_dom = element.dom;
while(current_dom.tagName != 'ARTICLE') {
if((current_dom.tagName == 'OL') || (current_dom.tagName == 'UL'))
extra_depth += 1;
current_dom = current_dom.parentElement.closest('ol,ul,article');
}
element.level = lastHeadingLevel+extra_depth;
}
else
lastHeadingLevel = element.level;
}); });
} }
@ -192,7 +220,7 @@ class TocNavBarUpdater {
let newNode = document.createElement('li'); let newNode = document.createElement('li');
const pathURL = location.pathname + '#' + pathItem.id + location.search; const pathURL = location.pathname + '#' + pathItem.id + location.search;
newNode.innerHTML = "<a href=" + pathURL + "> #<sub>" + pathItem.level + '</sub>' + pathItem.name + '</a>' newNode.innerHTML = "<a hx-boost=false href=" + pathURL + "> #<sub>" + pathItem.level + '</sub>' + pathItem.name + '</a>'
this.navbar_dom.appendChild(newNode); this.navbar_dom.appendChild(newNode);
this.added_navbar_elements.push(newNode); this.added_navbar_elements.push(newNode);
@ -224,17 +252,37 @@ class TocSidemenu {
} }
_generateSidebar() { _generateSidebar() {
this.toc_tracker.known_heading_elements.forEach(element => { let toc_stack = [this.sidebar_dom];
let new_element = document.createElement('li'); let last_added_li = null;
this.toc_tracker.known_heading_elements.forEach(element => {
while(element.level > toc_stack.length) {
if(!last_added_li) {
last_added_li = document.createElement('li');
toc_stack[toc_stack.length-1].appendChild(last_added_li);
}
let new_ol = document.createElement('ol');
last_added_li.appendChild(new_ol);
last_added_li = false;
toc_stack.push(new_ol);
}
while(element.level < toc_stack.length)
toc_stack.pop();
let new_element = document.createElement('li');
last_added_li = new_element;
const pathURL = location.pathname + '#' + element.id + location.search; const pathURL = location.pathname + '#' + element.id + location.search;
new_element.style = "padding-left: " + element.level * 0.8 + "em"; new_element.innerHTML = "<a hx-boost=false style=\"padding-left: " + element.level * 0.8 + "em\" href=" + pathURL + ">" + element.name + "</a>";
new_element.innerHTML = "<a href=" + pathURL + ">" + element.name + "</a>"; if(element.collapse)
new_element.classList.add('toc_collapsing');
this.sidebar_elements[element.id] = new_element; this.sidebar_elements[element.id] = new_element;
this.sidebar_dom.appendChild(new_element); toc_stack[toc_stack.length-1].appendChild(new_element);
}); });
} }
@ -251,13 +299,18 @@ class TocSidemenu {
if(this.currently_active_entry) { if(this.currently_active_entry) {
this.currently_active_entry.classList.remove('active'); this.currently_active_entry.classList.remove('active');
} }
Object.values(this.sidebar_elements).forEach((entry) => entry.classList.remove('contains_active'));
let active_entry = this.sidebar_elements[entry.dom.id]; let active_entry = this.sidebar_elements[entry.dom.id];
active_entry.classList.add('active'); active_entry.classList.add('active');
this.currently_active_entry = active_entry; this.currently_active_entry = active_entry;
entry.path.forEach((path_piece) => this.sidebar_elements[path_piece.id].classList.add('contains_active'));
} }
} }
let tracker = new TocTracker(); let tracker = new TocTracker();
let navbar_updater = new TocNavBarUpdater(tracker); let navbar_updater = new TocNavBarUpdater(tracker);
let sidebar_updater = new TocSidemenu(tracker); let sidebar_updater = new TocSidemenu(tracker);
document.addEventListener('DOMContentLoaded', (event) => tracker.reloadHeadings());