diff --git a/www/static/styles/toc.css b/www/static/styles/toc.css index b6f1fb7..129619e 100644 --- a/www/static/styles/toc.css +++ b/www/static/styles/toc.css @@ -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; } } \ No newline at end of file diff --git a/www/static/toc.js b/www/static/toc.js new file mode 100644 index 0000000..13175e2 --- /dev/null +++ b/www/static/toc.js @@ -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 = " #" + pathItem.level + '' + pathItem.name + '' + + 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 = "" + element.name + ""; + + 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); \ No newline at end of file diff --git a/www/templates/root.html b/www/templates/root.html index f0ab713..33dd9df 100644 --- a/www/templates/root.html +++ b/www/templates/root.html @@ -108,6 +108,6 @@ test? {% endblock %} - +