178 lines
5.6 KiB
JavaScript
178 lines
5.6 KiB
JavaScript
// This is definitely not a three.js ripoff.
|
|
|
|
import { addSpell } from "treehouse/spells.js";
|
|
import * as ulid from "treehouse/ulid.js";
|
|
|
|
/* Branch persistence */
|
|
|
|
const branchStateKey = "treehouse.openBranches";
|
|
let branchState = JSON.parse(sessionStorage.getItem(branchStateKey)) || {};
|
|
|
|
function saveBranchIsOpen(branchID, state) {
|
|
branchState[branchID] = state;
|
|
sessionStorage.setItem(branchStateKey, JSON.stringify(branchState));
|
|
}
|
|
|
|
function branchIsOpen(branchID) {
|
|
return branchState[branchID];
|
|
}
|
|
|
|
export class Branch {
|
|
static branchesByNamedID = new Map();
|
|
static onAdded = [];
|
|
|
|
constructor(element) {
|
|
this.element = element;
|
|
|
|
this.isLeaf = element.classList.contains("leaf");
|
|
|
|
this.details = element.childNodes[0];
|
|
this.innerUL = this.details.childNodes[1];
|
|
|
|
if (this.isLeaf) {
|
|
this.contentContainer = element.childNodes[0];
|
|
} else {
|
|
this.contentContainer = this.details.childNodes[0];
|
|
}
|
|
this.bulletPoint = this.contentContainer.childNodes[0];
|
|
this.branchContent = this.contentContainer.childNodes[1];
|
|
this.buttonBar = this.contentContainer.childNodes[2];
|
|
|
|
let doPersist = !element.hasAttribute("data-th-do-not-persist");
|
|
let isOpen = branchIsOpen(element.id);
|
|
if (doPersist && isOpen !== undefined) {
|
|
this.details.open = isOpen;
|
|
}
|
|
if (!this.isLeaf) {
|
|
this.details.addEventListener("toggle", (_) => {
|
|
saveBranchIsOpen(element.id, this.details.open);
|
|
});
|
|
}
|
|
|
|
this.namedID = element.id.split(":")[1];
|
|
Branch.branchesByNamedID.set(this.namedID, element);
|
|
|
|
if (ulid.isCanonicalUlid(this.namedID)) {
|
|
let timestamp = ulid.getTimestamp(this.namedID);
|
|
let date = document.createElement("span");
|
|
date.classList.add("branch-date");
|
|
date.innerText = timestamp.toLocaleDateString();
|
|
this.buttonBar.insertBefore(date, this.buttonBar.firstChild);
|
|
}
|
|
|
|
for (let callback of Branch.onAdded) {
|
|
callback(element, this);
|
|
}
|
|
}
|
|
}
|
|
|
|
addSpell("b", Branch);
|
|
|
|
/* Linked branches */
|
|
|
|
class LinkedBranch extends Branch {
|
|
static byLink = new Map();
|
|
|
|
constructor(element) {
|
|
super(element);
|
|
|
|
this.linkedTree = element.getAttribute("data-th-link");
|
|
LinkedBranch.byLink.set(this.linkedTree, this);
|
|
|
|
this.loadingText = document.createElement("p");
|
|
{
|
|
this.loadingText.className = "link-loading";
|
|
let linkedTreeName = document.createElement("code");
|
|
linkedTreeName.innerText = this.linkedTree;
|
|
this.loadingText.append("Loading ", linkedTreeName, "...");
|
|
}
|
|
this.innerUL.appendChild(this.loadingText);
|
|
|
|
// This produces a warning during static generation but we still want to handle that
|
|
// correctly, as Branch saves the state in localStorage. Having an expanded-by-default
|
|
// linked block can be useful in development.
|
|
if (this.details.open) {
|
|
this.loadTree("constructor");
|
|
}
|
|
|
|
this.details.addEventListener("toggle", (_) => {
|
|
if (this.details.open) {
|
|
this.loadTree("toggle");
|
|
}
|
|
});
|
|
}
|
|
|
|
async loadTreePromise(_initiator) {
|
|
try {
|
|
let response = await fetch(`${TREEHOUSE_SITE}/${this.linkedTree}`);
|
|
if (response.status == 404) {
|
|
throw `Hmm, seems like the tree "${this.linkedTree}" does not exist.`;
|
|
}
|
|
|
|
let text = await response.text();
|
|
let parser = new DOMParser();
|
|
let linkedDocument = parser.parseFromString(text, "text/html");
|
|
let main = linkedDocument.getElementsByTagName("main")[0];
|
|
let ul = main.getElementsByTagName("ul")[0];
|
|
let styles = main.getElementsByTagName("link");
|
|
let scripts = main.getElementsByTagName("script");
|
|
|
|
this.loadingText.remove();
|
|
this.innerUL.innerHTML = ul.innerHTML;
|
|
|
|
this.element.append(...styles);
|
|
for (let script of scripts) {
|
|
// No need to await for the import because we don't use the resulting module.
|
|
// Just fire and forger 💀
|
|
// and let them run in parallel.
|
|
let url = URL.createObjectURL(
|
|
new Blob([script.textContent], { type: "text/javascript" }),
|
|
);
|
|
import(url);
|
|
}
|
|
} catch (error) {
|
|
this.loadingText.innerText = error.toString();
|
|
}
|
|
}
|
|
|
|
loadTree() {
|
|
if (!this.loading) {
|
|
this.loading = this.loadTreePromise();
|
|
}
|
|
return this.loading;
|
|
}
|
|
}
|
|
|
|
addSpell("b-linked", LinkedBranch);
|
|
|
|
/* Fragment navigation */
|
|
|
|
function expandDetailsRecursively(element) {
|
|
while (element && element.tagName != "MAIN") {
|
|
if (element.tagName == "DETAILS") {
|
|
element.open = true;
|
|
}
|
|
element = element.parentElement;
|
|
}
|
|
}
|
|
|
|
function getCurrentlyHighlightedBranch() {
|
|
if (window.location.pathname == "/b" && window.location.search.length > 0) {
|
|
let shortID = window.location.search.substring(1);
|
|
return Branch.branchesByNamedID.get(shortID).id;
|
|
} else {
|
|
return window.location.hash.substring(1);
|
|
}
|
|
}
|
|
|
|
async function highlightCurrentBranch() {
|
|
let branch = document.getElementById(getCurrentlyHighlightedBranch());
|
|
if (branch != null) {
|
|
expandDetailsRecursively(branch);
|
|
branch.classList.add("target");
|
|
branch.scrollIntoView();
|
|
}
|
|
}
|
|
|
|
addEventListener("DOMContentLoaded", highlightCurrentBranch);
|