forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import * as Build from "./code.js";
2import * as Dashboard from "./dashboard.js";
3import * as Grid from "./grid.js";
4import * as Guide from "./guide.js";
5import * as Nav from "./nav.js";
6
7/** Base pathname of the app (e.g. "/" at root, "/diffuse/" in a subdirectory). */
8const BASE_PATHNAME = new URL(document.baseURI).pathname;
9
10/**
11 * Strips the app's base path prefix from an absolute pathname,
12 * returning a root-relative path like "/code".
13 *
14 * @param {string} pathname
15 */
16function relativePathname(pathname) {
17 const stripped = pathname.replace(/\/$/, "");
18 const base = BASE_PATHNAME.replace(/\/$/, "");
19 return base.length > 0 && stripped.startsWith(base)
20 ? stripped.slice(base.length)
21 : stripped;
22}
23
24/**
25 * @param {URL} url
26 */
27async function initJsBasedOnPage(url) {
28 const path = relativePathname(url.pathname);
29
30 Nav.update();
31 Nav.updateActiveLinks();
32 Nav.watchResize();
33
34 Grid.setupFilter();
35 Grid.insertToggleButtons();
36 await Grid.monitorToggleButtonStates();
37 await Grid.setupOutputIndicator();
38
39 switch (path) {
40 case "/code":
41 Build.renderEditor();
42 Build.handleBuildFormSubmit();
43 Build.listenForExamplesEdit();
44 await Build.editFacetFromURL();
45 break;
46 case "/dashboard":
47 await Dashboard.renderList();
48 break;
49 case "/guide":
50 Guide.setupSampleButton();
51 break;
52 default:
53 break;
54 }
55}
56
57initJsBasedOnPage(new URL(location.href));
58
59// Partial page updates for kitchen navigation using the Navigation API.
60// Intercepts nav link clicks, fetches the new page, and swaps <main> content
61// instead of doing a full page load.
62
63if ("navigation" in globalThis) {
64 /** @type {any} */ (globalThis).navigation.addEventListener(
65 "navigate",
66 navigateHandler,
67 );
68}
69
70/** @param {any} event */
71function navigateHandler(event) {
72 if (!event.canIntercept) return;
73
74 const url = new URL(event.destination.url);
75 if (url.origin !== location.origin) return;
76 if (url.pathname === location.pathname) return;
77
78 // Only intercept paths one level deep
79 const relative = relativePathname(url.pathname);
80 const parts = relative.split("/").filter(Boolean);
81 if (parts.length === 0) return;
82 if (parts.length > 2) return;
83
84 // Skip the loader page
85 if (parts[0] === "l") return;
86 if (parts.includes("chronicle")) return;
87
88 event.intercept({
89 scroll: "manual",
90 async handler() {
91 const navLinks = /** @type {HTMLAnchorElement[]} */ ([
92 ...document.querySelectorAll("#diffuse-nav a, #nav-overflow-menu a"),
93 ]);
94 const stripSlash = (/** @type {string} */ p) => p.replace(/^\//, "");
95 const navLink = navLinks.find(
96 (a) =>
97 stripSlash(new URL(a.href).pathname) === stripSlash(url.pathname),
98 );
99
100 const icon = navLink?.querySelector("i");
101 const originalIconClass = icon?.className;
102 let addedSpinner = /** @type {HTMLElement | undefined} */ (undefined);
103
104 const loadingTimer = navLink
105 ? setTimeout(() => {
106 if (icon) {
107 icon.className = "ph-bold ph-spinner animate-spin";
108 } else {
109 addedSpinner = document.createElement("i");
110 addedSpinner.className = "ph-bold ph-spinner animate-spin";
111 const span = navLink.querySelector("span");
112 (span ?? navLink).prepend(addedSpinner);
113 }
114 }, 250)
115 : undefined;
116
117 let html;
118
119 try {
120 const response = await fetch(url);
121 if (!response.ok) throw new Error(`${response.status}`);
122 html = await response.text();
123 } catch {
124 clearTimeout(loadingTimer);
125 if (icon && originalIconClass !== undefined) icon.className = originalIconClass;
126 addedSpinner?.remove();
127 location.href = url.href;
128 return;
129 } finally {
130 clearTimeout(loadingTimer);
131 if (icon && originalIconClass !== undefined) icon.className = originalIconClass;
132 addedSpinner?.remove();
133 }
134
135 const parser = new DOMParser();
136 const doc = parser.parseFromString(html, "text/html");
137
138 const newMain = doc.querySelector("main");
139 const currentMain = document.querySelector("main");
140
141 if (!newMain || !currentMain) {
142 location.href = url.href;
143 return;
144 }
145
146 document.title = doc.title;
147
148 // Replace <main> content
149 const range = document.createRange();
150 range.selectNode(currentMain);
151 const documentFragment = range.createContextualFragment(
152 newMain.innerHTML ?? "",
153 );
154
155 currentMain.innerHTML = "";
156 currentMain.append(documentFragment);
157
158 initJsBasedOnPage(url);
159
160 window.scrollTo({ top: 0, behavior: "instant" });
161 },
162 });
163}