2023-08-27 15:59:52 +02:00
|
|
|
// This is definitely not a three.js ripoff.
|
|
|
|
|
2023-08-27 14:50:46 +02:00
|
|
|
import { navigationMap } from "/navmap.js";
|
|
|
|
|
2023-09-03 11:45:14 +02:00
|
|
|
/* Branch persistence */
|
|
|
|
|
2023-08-22 19:23:31 +02:00
|
|
|
const branchStateKey = "treehouse.openBranches";
|
2023-08-29 14:28:18 +02:00
|
|
|
let branchState = JSON.parse(sessionStorage.getItem(branchStateKey)) || {};
|
2023-08-22 19:23:31 +02:00
|
|
|
|
|
|
|
function saveBranchIsOpen(branchID, state) {
|
|
|
|
branchState[branchID] = state;
|
2023-08-29 14:28:18 +02:00
|
|
|
sessionStorage.setItem(branchStateKey, JSON.stringify(branchState));
|
2023-08-22 19:23:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function branchIsOpen(branchID) {
|
|
|
|
return branchState[branchID];
|
|
|
|
}
|
|
|
|
|
|
|
|
class Branch extends HTMLLIElement {
|
2023-08-20 15:05:59 +02:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
|
|
|
|
this.details = this.childNodes[0];
|
|
|
|
this.innerUL = this.details.childNodes[1];
|
|
|
|
|
2023-08-22 22:32:40 +02:00
|
|
|
let doPersist = !this.hasAttribute("data-th-do-not-persist");
|
2023-08-22 19:23:31 +02:00
|
|
|
let isOpen = branchIsOpen(this.id);
|
2023-08-22 22:32:40 +02:00
|
|
|
if (doPersist && isOpen !== undefined) {
|
2023-08-22 19:23:31 +02:00
|
|
|
this.details.open = isOpen;
|
|
|
|
}
|
|
|
|
this.details.addEventListener("toggle", _ => {
|
|
|
|
saveBranchIsOpen(this.id, this.details.open);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
customElements.define("th-b", Branch, { extends: "li" });
|
|
|
|
|
2023-09-03 11:45:14 +02:00
|
|
|
/* Linked branches */
|
|
|
|
|
2023-08-22 19:23:31 +02:00
|
|
|
class LinkedBranch extends Branch {
|
2023-08-27 14:50:46 +02:00
|
|
|
static byLink = new Map();
|
|
|
|
|
2023-08-22 19:23:31 +02:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
|
|
|
|
this.linkedTree = this.getAttribute("data-th-link");
|
2023-08-27 14:50:46 +02:00
|
|
|
LinkedBranch.byLink.set(this.linkedTree, this);
|
2023-08-22 19:23:31 +02:00
|
|
|
|
|
|
|
this.loadingState = "notloaded";
|
2023-08-20 15:05:59 +02:00
|
|
|
|
|
|
|
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
|
2023-08-22 19:23:31 +02:00
|
|
|
// correctly, as Branch saves the state in localStorage. Having an expanded-by-default
|
|
|
|
// linked block can be useful in development.
|
2023-08-20 15:05:59 +02:00
|
|
|
if (this.details.open) {
|
2023-08-27 14:50:46 +02:00
|
|
|
this.loadTree("constructor");
|
2023-08-20 15:05:59 +02:00
|
|
|
}
|
|
|
|
|
2023-08-22 19:23:31 +02:00
|
|
|
this.details.addEventListener("toggle", _ => {
|
2023-08-20 15:05:59 +02:00
|
|
|
if (this.details.open) {
|
2023-08-27 14:50:46 +02:00
|
|
|
this.loadTree("toggle");
|
2023-08-20 15:05:59 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-08-27 14:50:46 +02:00
|
|
|
async loadTreePromise(_initiator) {
|
|
|
|
try {
|
2023-08-27 15:59:52 +02:00
|
|
|
let response = await fetch(`${TREEHOUSE_SITE}/${this.linkedTree}.html`);
|
2023-08-27 14:50:46 +02:00
|
|
|
if (response.status == 404) {
|
|
|
|
throw `Hmm, seems like the tree "${this.linkedTree}" does not exist.`;
|
|
|
|
}
|
2023-08-20 15:05:59 +02:00
|
|
|
|
2023-08-27 14:50:46 +02:00
|
|
|
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];
|
|
|
|
|
|
|
|
this.loadingText.remove();
|
|
|
|
this.innerUL.innerHTML = ul.innerHTML;
|
|
|
|
} catch (error) {
|
|
|
|
this.loadingText.innerText = error.toString();
|
2023-08-20 15:05:59 +02:00
|
|
|
}
|
|
|
|
}
|
2023-08-27 14:50:46 +02:00
|
|
|
|
|
|
|
loadTree() {
|
|
|
|
if (!this.loading) {
|
|
|
|
this.loading = this.loadTreePromise();
|
|
|
|
}
|
|
|
|
return this.loading;
|
|
|
|
}
|
2023-08-20 15:05:59 +02:00
|
|
|
}
|
|
|
|
|
2023-08-22 19:23:31 +02:00
|
|
|
customElements.define("th-b-linked", LinkedBranch, { extends: "li" });
|
2023-08-21 21:12:06 +02:00
|
|
|
|
2023-09-03 11:45:14 +02:00
|
|
|
/* Fragment navigation */
|
|
|
|
|
|
|
|
let rehashing = false;
|
|
|
|
function rehash() { // https://www.youtube.com/watch?v=Tv1SYqLllKI
|
|
|
|
if (!rehashing) {
|
|
|
|
rehashing = true;
|
|
|
|
let hash = window.location.hash;
|
|
|
|
window.location.hash = "";
|
|
|
|
window.location.hash = hash;
|
|
|
|
rehashing = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-21 21:12:06 +02:00
|
|
|
function expandDetailsRecursively(element) {
|
|
|
|
while (element && element.tagName != "MAIN") {
|
|
|
|
if (element.tagName == "DETAILS") {
|
|
|
|
element.open = true;
|
|
|
|
}
|
|
|
|
element = element.parentElement;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-27 14:50:46 +02:00
|
|
|
function navigateToPage(page) {
|
2023-08-27 15:59:52 +02:00
|
|
|
window.location.href = `${TREEHOUSE_SITE}/${page}.html`
|
2023-08-27 14:50:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function navigateToBranch(fragment) {
|
2023-09-03 11:45:14 +02:00
|
|
|
if (fragment.length == 0) return;
|
2023-08-27 14:50:46 +02:00
|
|
|
|
|
|
|
let element = document.getElementById(fragment);
|
|
|
|
if (element !== null) {
|
2023-08-21 21:12:06 +02:00
|
|
|
// If the element is already loaded on the page, we're good.
|
|
|
|
expandDetailsRecursively(element);
|
2023-09-03 11:45:14 +02:00
|
|
|
rehash();
|
2023-08-27 14:50:46 +02:00
|
|
|
} else {
|
|
|
|
// The element is not loaded, we need to load the tree that has it.
|
|
|
|
let parts = fragment.split(':');
|
|
|
|
if (parts.length >= 2) {
|
|
|
|
let [page, _id] = parts;
|
|
|
|
let fullPath = navigationMap[page];
|
|
|
|
if (Array.isArray(fullPath)) {
|
|
|
|
// TODO: This logic will probably need to be upgraded at some point to support
|
|
|
|
// navigation maps with roots other than index. Currently though only index is
|
|
|
|
// generated so that doesn't matter.
|
|
|
|
let [_root, ...path] = fullPath;
|
|
|
|
if (path !== undefined) {
|
|
|
|
let isNotAtIndexHtml = window.location.pathname != "/index.html";
|
2023-08-27 18:25:21 +02:00
|
|
|
let lastBranch = null;
|
2023-08-27 14:50:46 +02:00
|
|
|
for (let linked of path) {
|
|
|
|
let branch = LinkedBranch.byLink.get(linked);
|
|
|
|
|
|
|
|
if (isNotAtIndexHtml && branch === undefined) {
|
|
|
|
navigateToPage("index");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await branch.loadTree("navigateToBranch");
|
2023-08-27 18:25:21 +02:00
|
|
|
lastBranch = branch;
|
|
|
|
}
|
|
|
|
if (lastBranch != null) {
|
|
|
|
expandDetailsRecursively(lastBranch.details);
|
2023-08-27 14:50:46 +02:00
|
|
|
}
|
2023-09-03 11:45:14 +02:00
|
|
|
rehash();
|
2023-08-27 14:50:46 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// In case the navigation map does not contain the given page, we can try
|
|
|
|
// redirecting the user to a concrete page on the site.
|
|
|
|
navigateToPage(page);
|
|
|
|
}
|
|
|
|
}
|
2023-08-21 21:12:06 +02:00
|
|
|
}
|
2023-08-27 14:50:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function navigateToCurrentBranch() {
|
|
|
|
let location = window.location.hash.substring(1);
|
2023-09-03 11:45:14 +02:00
|
|
|
await navigateToBranch(location);
|
2023-08-27 14:50:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// When you click on a link, and the destination is within a <details> that is not expanded,
|
|
|
|
// expand the <details> recursively.
|
|
|
|
window.addEventListener("popstate", navigateToCurrentBranch);
|
|
|
|
addEventListener("DOMContentLoaded", navigateToCurrentBranch);
|