// 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());