2025-01-16 23:28:37 +01:00
|
|
|
// 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();
|
|
|
|
|
2025-02-03 10:47:32 +01:00
|
|
|
this.navbar_dom = document.querySelector('#main_navbar_path_list');
|
2025-01-16 23:28:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
trackingUpdateCallback(element) {
|
|
|
|
this._removeNavbarElements();
|
|
|
|
|
|
|
|
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>'
|
|
|
|
|
2025-02-03 10:47:32 +01:00
|
|
|
this.navbar_dom.appendChild(newNode);
|
2025-01-16 23:28:37 +01:00
|
|
|
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);
|