feat: added TOC :D

This commit is contained in:
David Bailey 2025-01-16 23:28:37 +01:00
parent 31150b9b12
commit 771e9a2ec8
3 changed files with 294 additions and 8 deletions

View file

@ -1,10 +1,25 @@
.table_of_contents {
position: sticky;
float: left;
top: 3em;
top: 0px;
width: 20em;
width: calc(var(--content-margin) - 2em);
margin-left: 2em;
padding-top: 0.8em;
padding-right: 0.5em;
padding-bottom: 0.5em;
border-radius: 0 0em 0em 0.5em;
box-shadow: -0.2em 0.2em 0.2em black;
--toc-fg: #cecece;
--toc-bg: #EE901544;
background-color: var(--bg_2);
& a {
display: inline-block;
@ -19,23 +34,29 @@
padding-right: 0.2em;
padding-bottom: 0.2em;
border-radius: 0.2em;
color: var(--toc-fg) !important;
}
& .active {
background-color: var(--highlight_1);
color: var(--bg_1);
background-color: var(--toc-bg);
border-bottom: 1px solid var(--highlight_1);
transition: all 0.2s;
}
& ol {
padding-left: 0.5em;
list-style: none;
border-left: 1px solid #FFFFFF44;
}
& li {
border-radius: 0.2em;
border-bottom: 1px solid transparent;
margin-bottom: 0.2em;
transition: all 1s;
}
}

265
www/static/toc.js Normal file
View file

@ -0,0 +1,265 @@
// Code By Webdevtrick ( https://webdevtrick.com )
// https://webdevtrick.com/dynamic-table-of-contents/
//
// Modified by dergens
let tocId = "toc";
const TOC_IDENTIFIER = "toc";
const TOC_MAIN_CONTENT_ID = "#content_article"
const TOC_TOP_PIXEL_MARGIN = 300;
class TocTracker {
known_heading_elements = [];
constructor() {
this.intersection_observer = null;
this.tracking_update_callbacks = [];
this.tracking_rebuild_callbacks = [];
}
_disconnectIntersectionObserver() {
if(this.intersection_observer) {
this.intersection_observer.disconnect();
this.intersection_observer = null;
}
}
_setupIntersectionObserver() {
let options = {
root: null,
rootMargin: `-${TOC_TOP_PIXEL_MARGIN}px 0px 0px 0px`,
threshold: [1]
};
this.intersection_observer = new IntersectionObserver(
(entries) => this._findActiveHeading(),
options
);
this.known_heading_elements.forEach((element) => {
this.intersection_observer.observe(element.dom);
});
}
_searchForHeadings() {
const main = document.querySelector(TOC_MAIN_CONTENT_ID);
if (!main) {
throw Error("A `main` tag section is required to query headings from.");
}
let headings = main.querySelectorAll("h1, h2, h3, h4, h5, h6");
headings.forEach((heading, index) => {
const heading_level = parseInt(heading.tagName.slice(-1));
this.known_heading_elements.push({
level: heading_level,
name: heading.innerHTML,
dom: heading,
});
});
}
_sortHeadings() {
this.known_heading_elements.sort((a, b) => {
return a.dom.getBoundingClientRect().y
- b.dom.getBoundingClientRect().y;
});
}
_generateHeadingPaths() {
let current_path = [];
this.known_heading_elements.forEach(heading => {
while((current_path.length != 0) && (current_path[current_path.length-1].level >= heading.level))
current_path.pop();
current_path.push(heading);
// The zero does nothing. But according to stackoverflow, it makes it faster.
// Why? No idea.
// Just leave it in I suppose.
heading.path = current_path.slice(0);
});
}
_fillHeadingIDs() {
this.known_heading_elements.forEach(heading => {
heading.id = heading.dom.id;
if(heading.id)
return;
heading.dom.id = heading.path.map(i =>
i.name.replace(/\W+/g, '-').trim()
).join('--').toLowerCase();
heading.id = heading.dom.id;
});
}
_findActiveHeading() {
const arr = this.known_heading_elements;
if(arr.length == 0)
return;
// Start should always be higher
// End always lower
let start = 0, end = arr.length - 1;
let mid = Math.floor((start + end) / 2);
// Special case handling, if all items are above the scroll margin
if(arr[end].dom.getBoundingClientRect().y < TOC_TOP_PIXEL_MARGIN) {
start = end;
mid = end;
}
// Iterate until we find the boundary
while (mid > start) {
// Check if the mid is above our boundary ((lower Y))
if (arr[mid].dom.getBoundingClientRect().y < TOC_TOP_PIXEL_MARGIN)
start = mid;
else
end = mid;
// Find the mid index
mid = Math.floor((start + end) / 2);
}
this.tracking_update_callbacks.forEach(callback => callback(arr[start]));
return arr[start];
}
reloadHeadings() {
this.known_heading_elements = [];
this._disconnectIntersectionObserver();
this._searchForHeadings();
this._sortHeadings();
this._generateHeadingPaths();
this._fillHeadingIDs();
this.tracking_rebuild_callbacks.forEach(item => item());
this._setupIntersectionObserver();
}
onTrackingUpdate(callback) {
this.tracking_update_callbacks.push(callback);
return callback;
}
onTrackingRebuild(callback) {
this.tracking_rebuild_callbacks.push(callback);
return callback;
}
}
class TocNavBarUpdater {
constructor(toc_tracker) {
this.toc_tracker = toc_tracker;
this.navbar_dom = null;
this.added_navbar_elements = [];
this.toc_tracker.onTrackingRebuild(() =>
this.trackingRebuildCallback() );
this.toc_tracker.onTrackingUpdate((element) => this.trackingUpdateCallback(element) );
}
_removeNavbarElements() {
this.added_navbar_elements.forEach(dom => dom.remove());
this.added_navbar_elements = [];
}
trackingRebuildCallback() {
this._removeNavbarElements();
this.navbar_dom = document.querySelector('.navbar ._path');
}
trackingUpdateCallback(element) {
this._removeNavbarElements();
const navbar_prev_node = this.navbar_dom.children[this.navbar_dom.children.length -1];
element.path.forEach(pathItem => {
let newNode = document.createElement('li');
const pathURL = location.pathname + '#' + pathItem.id + location.search;
newNode.innerHTML = "<a href=" + pathURL + "> #<sub>" + pathItem.level + '</sub>' + pathItem.name + '</a>'
this.navbar_dom.insertBefore(newNode, navbar_prev_node);
this.added_navbar_elements.push(newNode);
});
}
}
class TocSidemenu {
constructor(toc_tracker) {
this.toc_tracker = toc_tracker;
this.sidebar_dom = null;
this.sidebar_elements = {};
this.currently_active_entry = null;
toc_tracker.onTrackingRebuild(() => this.trackingRebuildCallback());
toc_tracker.onTrackingUpdate((element) => this.trackingUpdateCallback(element));
}
_clearSidebar() {
if(this.sidebar_dom) {
this.sidebar_dom.remove();
}
this.sidebar_elements = {};
this.currently_active_entry = null;
}
_generateSidebar() {
this.toc_tracker.known_heading_elements.forEach(element => {
let new_element = document.createElement('li');
const pathURL = location.pathname + '#' + element.id + location.search;
new_element.style = "padding-left: " + element.level * 0.8 + "em";
new_element.innerHTML = "<a href=" + pathURL + ">" + element.name + "</a>";
this.sidebar_elements[element.id] = new_element;
this.sidebar_dom.appendChild(new_element);
});
}
trackingRebuildCallback() {
this._clearSidebar();
this.sidebar_dom = document.createElement('ol');
document.querySelector('#toc').appendChild(this.sidebar_dom);
this._generateSidebar();
}
trackingUpdateCallback(entry) {
if(this.currently_active_entry) {
this.currently_active_entry.classList.remove('active');
}
let active_entry = this.sidebar_elements[entry.dom.id];
active_entry.classList.add('active');
this.currently_active_entry = active_entry;
}
}
let tracker = new TocTracker();
let navbar_updater = new TocNavBarUpdater(tracker);
let sidebar_updater = new TocSidemenu(tracker);

View file

@ -108,6 +108,6 @@
<span> test? </span>
{% endblock %}
</footer>
<script> tracker.reloadHeadings(); </script>
</body>
</html>