theophile
Version:
A templating module that transforms a web page into a (Powerpoint-like) presentation
106 lines (104 loc) • 3.86 kB
JavaScript
import Plugin from "../Plugin.js";
//TODO #23 When skipping headings (like h1+h3) collapsing doesn't work. Don't have the time to fix now
//TODO #24 Make scrolling smouth. For now, il scroll-behavious is smouth, page scrolls from top after ending slideshow.
//TODO #25 Toc: Make it possible to pin TOC on the page
export default class Toc extends Plugin {
static async init(Theophile) {
await super.init(Theophile);
this.headings = this.headings || "h1,h2,h3";
}
static async process() {
await super.process();
this._headings = Array.from(document.body.querySelectorAll(this.headings));
this.hierarchy = this.getHierarchy(this._headings);
}
static async afterMount() {
await super.afterMount();
const tocContainer = document.querySelector("#th-toc");
if (!tocContainer) return;
const btnPin = tocContainer.appendChild(document.createElement("span"));
btnPin.classList.add("th-toc-btn-pin");
btnPin.addEventListener("click", _e => {
document.documentElement.classList.toggle("th-toc-pin");
});
tocContainer.appendChild(this.html);
}
static async clean() {
super.clean();
delete this.hierarchy;
window.addEventListener("scroll", _e => {
const visible = this.findVisibleHeading();
if (visible.tocElement.classList.contains("th-toc-current")) {
return;
}
document.querySelectorAll(".th-toc-current, .th-toc-current-within").forEach(element => element.classList.remove("th-toc-current", "th-toc-current-within"));
visible.tocElement.classList.add("th-toc-current");
var ptr = visible.tocElement;
while (ptr) {
if (ptr.id === "th-toc") break;
ptr.classList.add("th-toc-current-within");
ptr = ptr.parentNode.closest("li");
}
});
}
static get html() {
return this.html_ul(this.hierarchy);
}
static html_ul(group, level = 1) {
const result = document.createElement("ul");
group.forEach(headingObject => {
const li = result.appendChild(document.createElement("li"));
li.classList.add("th-toc-level-" + level);
if (headingObject.heading) {
headingObject.heading.tocElement = li;
li.destination = headingObject.heading;
const div = li.appendChild(document.createElement("div"));
const a = div.appendChild(document.createElement("a"));
a.href = "#" + headingObject.heading.id;
a.innerHTML = headingObject.heading.innerText;
} else {
li.classList.add("th-toc-no-heading");
}
if (headingObject.group.length) {
li.appendChild(this.html_ul(headingObject.group, level + 1));
}
});
return result;
}
static getHierarchy(nodeList) {
const result = [];
var currentLevel = 0;
const path = [result];
nodeList.forEach(heading => {
let level = parseInt(heading.tagName[1]);
const headingObject = { heading: heading, group: [] };
while (level > currentLevel + 1) {
const empty = { heading: null, group: [] };
path[currentLevel].push(empty);
currentLevel += 1;
path[currentLevel] = empty.group;
}
if (level === currentLevel) {
path[currentLevel - 1].push(headingObject);
path[currentLevel] = headingObject.group;
} else if (level < currentLevel) {
currentLevel = level;
path[currentLevel - 1].push(headingObject);
path[currentLevel] = headingObject.group;
} else if (level === currentLevel + 1) {
path[currentLevel].push(headingObject);
currentLevel = level;
path[currentLevel] = headingObject.group;
}
});
return result;
}
static findVisibleHeading() {
var headings = this._headings.map(heading => {
return [heading, heading.getBoundingClientRect().y];
}).sort((a, b) => (a[1] < b[1] ? -1 : 1));
var last = headings.slice(-1)[0];
headings = headings.filter(heading => heading[1] >= 0);
return (headings[0] || last)[0];
}
}