// 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) { branch.scrollIntoView(); // Expand the linked branch so that the person entering the link doesn't have to click on it. expandDetailsRecursively(branch); if (branch.children.length > 0 && branch.children[0].tagName == "DETAILS") { expandDetailsRecursively(branch.children[0]); } branch.classList.add("target"); } } addEventListener("DOMContentLoaded", highlightCurrentBranch);