// 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 = 150;
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, li strong");
headings.forEach((heading, index) => {
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({
level: heading_level,
name: heading_name,
dom: heading,
collapse: heading_collapsed
});
});
}
_sortHeadings() {
this.known_heading_elements.sort((a, b) => {
return a.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;
});
}
_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('#main_navbar_path_list');
}
trackingUpdateCallback(element) {
this._removeNavbarElements();
element.path.forEach(pathItem => {
let newNode = document.createElement('li');
const pathURL = location.pathname + '#' + pathItem.id + location.search;
newNode.innerHTML = " #" + pathItem.level + '' + pathItem.name + ''
this.navbar_dom.appendChild(newNode);
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() {
let toc_stack = [this.sidebar_dom];
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;
new_element.innerHTML = "" + element.name + "";
if(element.collapse)
new_element.classList.add('toc_collapsing');
this.sidebar_elements[element.id] = new_element;
toc_stack[toc_stack.length-1].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');
}
Object.values(this.sidebar_elements).forEach((entry) => entry.classList.remove('contains_active'));
let active_entry = this.sidebar_elements[entry.dom.id];
active_entry.classList.add('active');
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 navbar_updater = new TocNavBarUpdater(tracker);
let sidebar_updater = new TocSidemenu(tracker);
document.addEventListener('DOMContentLoaded', (event) => tracker.reloadHeadings());