use turbo instead of my dog shit soft nav
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "overlord-server",
|
"name": "overlord-server",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hotwired/turbo": "^8.0.23",
|
||||||
"@msgpack/msgpack": "^3.0.0",
|
"@msgpack/msgpack": "^3.0.0",
|
||||||
"@solana/web3.js": "^1.98.4",
|
"@solana/web3.js": "^1.98.4",
|
||||||
"adm-zip": "^0.5.12",
|
"adm-zip": "^0.5.12",
|
||||||
@@ -62,6 +63,8 @@
|
|||||||
|
|
||||||
"@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@6.5.2", "", {}, "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q=="],
|
"@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@6.5.2", "", {}, "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q=="],
|
||||||
|
|
||||||
|
"@hotwired/turbo": ["@hotwired/turbo@8.0.23", "", {}, "sha512-GZ7cijxEZ6Ig71u7rD6LHaRv/wcE/hNsc+nEfiWOkLNqUgLOwo5MNGWOy5ZV9ZUDSiQx1no7YxjTNnT4O6//cQ=="],
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hotwired/turbo": "^8.0.23",
|
||||||
"@msgpack/msgpack": "^3.0.0",
|
"@msgpack/msgpack": "^3.0.0",
|
||||||
"@solana/web3.js": "^1.98.4",
|
"@solana/web3.js": "^1.98.4",
|
||||||
"adm-zip": "^0.5.12",
|
"adm-zip": "^0.5.12",
|
||||||
|
|||||||
@@ -2294,6 +2294,7 @@ function showBuildFiles(files, buildId, expiresAt) {
|
|||||||
|
|
||||||
const downloadBtn = document.createElement("a");
|
const downloadBtn = document.createElement("a");
|
||||||
downloadBtn.href = `/api/build/download/${encodeURIComponent(file.name)}`;
|
downloadBtn.href = `/api/build/download/${encodeURIComponent(file.name)}`;
|
||||||
|
downloadBtn.download = "";
|
||||||
downloadBtn.className =
|
downloadBtn.className =
|
||||||
"inline-flex items-center gap-1 px-3 py-1 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm transition-colors";
|
"inline-flex items-center gap-1 px-3 py-1 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm transition-colors";
|
||||||
downloadBtn.innerHTML = '<i class="fa-solid fa-download"></i> Download';
|
downloadBtn.innerHTML = '<i class="fa-solid fa-download"></i> Download';
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ function close() {
|
|||||||
function navigate(href, newTab = false) {
|
function navigate(href, newTab = false) {
|
||||||
pushRecent(href);
|
pushRecent(href);
|
||||||
if (newTab) window.open(href, "_blank", "noopener");
|
if (newTab) window.open(href, "_blank", "noopener");
|
||||||
else if (window.overlordSoftNavigate) window.overlordSoftNavigate(href);
|
else if (window.Turbo?.visit) window.Turbo.visit(href);
|
||||||
else window.location.href = href;
|
else window.location.href = href;
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ import { applyUserRoleUI, applyThumbnailWallVisibility } from "./nav/role-ui.js"
|
|||||||
import { loadPluginNavItems } from "./nav/plugins-loader.js";
|
import { loadPluginNavItems } from "./nav/plugins-loader.js";
|
||||||
import { init as initCommandPalette } from "./command-palette.js";
|
import { init as initCommandPalette } from "./command-palette.js";
|
||||||
import {
|
import {
|
||||||
installPageResourceTracker,
|
|
||||||
runWithoutPageTracking,
|
runWithoutPageTracking,
|
||||||
setupSoftNavigation,
|
setupTurboNavigation,
|
||||||
startPageTracking,
|
turboVisit,
|
||||||
} from "./soft-nav.js";
|
} from "./turbo-navigation.js";
|
||||||
|
|
||||||
const host = document.getElementById("top-nav");
|
const host = document.getElementById("top-nav");
|
||||||
if (host) {
|
if (host) {
|
||||||
@@ -112,11 +111,7 @@ if (host) {
|
|||||||
if (refs.accountSettingsBtn && !refs.accountSettingsBtn.dataset.boundSettings) {
|
if (refs.accountSettingsBtn && !refs.accountSettingsBtn.dataset.boundSettings) {
|
||||||
refs.accountSettingsBtn.dataset.boundSettings = "true";
|
refs.accountSettingsBtn.dataset.boundSettings = "true";
|
||||||
refs.accountSettingsBtn.addEventListener("click", () => {
|
refs.accountSettingsBtn.addEventListener("click", () => {
|
||||||
if (window.overlordSoftNavigate) {
|
turboVisit("/settings");
|
||||||
window.overlordSoftNavigate("/settings");
|
|
||||||
} else {
|
|
||||||
window.location.href = "/settings";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,32 +155,6 @@ if (host) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const prefetchedPages = new Set();
|
|
||||||
const prefetchPage = (href) => {
|
|
||||||
try {
|
|
||||||
const url = new URL(href, window.location.href);
|
|
||||||
if (url.origin !== window.location.origin) return;
|
|
||||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
|
||||||
const key = `${url.pathname}${url.search}`;
|
|
||||||
if (prefetchedPages.has(key)) return;
|
|
||||||
prefetchedPages.add(key);
|
|
||||||
const link = document.createElement("link");
|
|
||||||
link.rel = "prefetch";
|
|
||||||
link.as = "document";
|
|
||||||
link.href = key;
|
|
||||||
document.head.appendChild(link);
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
|
|
||||||
host.addEventListener("pointerenter", (event) => {
|
|
||||||
const link = event.target?.closest?.("a[href]");
|
|
||||||
if (link) prefetchPage(link.getAttribute("href"));
|
|
||||||
}, true);
|
|
||||||
host.addEventListener("focusin", (event) => {
|
|
||||||
const link = event.target?.closest?.("a[href]");
|
|
||||||
if (link) prefetchPage(link.getAttribute("href"));
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadCurrentUser() {
|
async function loadCurrentUser() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/auth/me", { credentials: "include" });
|
const res = await fetch("/api/auth/me", { credentials: "include" });
|
||||||
@@ -243,14 +212,12 @@ if (host) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
installPageResourceTracker();
|
setupTurboNavigation({
|
||||||
setupSoftNavigation(host, {
|
|
||||||
onPathChange: (path) => {
|
onPathChange: (path) => {
|
||||||
applyActivePath(path);
|
applyActivePath(path);
|
||||||
applyBranding();
|
applyBranding();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
startPageTracking();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyBranding() {
|
async function applyBranding() {
|
||||||
|
|||||||
@@ -1,357 +0,0 @@
|
|||||||
let installedTracker = false;
|
|
||||||
let pageTrackingEnabled = false;
|
|
||||||
let navSeq = 0;
|
|
||||||
|
|
||||||
const tracked = {
|
|
||||||
events: new Set(),
|
|
||||||
intervals: new Set(),
|
|
||||||
sockets: new Set(),
|
|
||||||
workers: new Set(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const original = {};
|
|
||||||
const loadedClassicScripts = new Set();
|
|
||||||
const persistentIds = new Set([
|
|
||||||
"top-nav",
|
|
||||||
"sb-mobile-bar",
|
|
||||||
"sb-backdrop",
|
|
||||||
"nav-reveal-btn",
|
|
||||||
"chat-bubble",
|
|
||||||
"chat-panel",
|
|
||||||
"cert-trust-banner",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function normalizeUrl(value, base = window.location.href) {
|
|
||||||
try {
|
|
||||||
return new URL(value, base).href;
|
|
||||||
} catch {
|
|
||||||
return value || "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scriptPath(value) {
|
|
||||||
try {
|
|
||||||
return new URL(value, window.location.href).pathname;
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldSkipScript(script) {
|
|
||||||
const path = scriptPath(script.getAttribute("src") || "");
|
|
||||||
return path === "/assets/nav.js" || path === "/assets/nav-prelude.js";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPersistentNode(node) {
|
|
||||||
return node instanceof Element && persistentIds.has(node.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupTrackedResources() {
|
|
||||||
pageTrackingEnabled = false;
|
|
||||||
window.dispatchEvent(new Event("pagehide"));
|
|
||||||
|
|
||||||
for (const id of tracked.intervals) original.clearInterval(id);
|
|
||||||
for (const entry of tracked.events) {
|
|
||||||
original.removeEventListener.call(entry.target, entry.type, entry.listener, entry.options);
|
|
||||||
}
|
|
||||||
for (const socket of tracked.sockets) {
|
|
||||||
try {
|
|
||||||
socket.onopen = null;
|
|
||||||
socket.onmessage = null;
|
|
||||||
socket.onerror = null;
|
|
||||||
socket.onclose = null;
|
|
||||||
socket.close(1000, "soft navigation");
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
for (const worker of tracked.workers) {
|
|
||||||
try {
|
|
||||||
worker.terminate();
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracked.events.clear();
|
|
||||||
tracked.intervals.clear();
|
|
||||||
tracked.sockets.clear();
|
|
||||||
tracked.workers.clear();
|
|
||||||
|
|
||||||
document.querySelectorAll("script[data-soft-nav-script]").forEach((script) => script.remove());
|
|
||||||
document.getElementById("cmdp-root")?.remove();
|
|
||||||
pageTrackingEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runWithoutPageTracking(fn) {
|
|
||||||
const previous = pageTrackingEnabled;
|
|
||||||
pageTrackingEnabled = false;
|
|
||||||
try {
|
|
||||||
const result = fn();
|
|
||||||
if (result && typeof result.then === "function") {
|
|
||||||
return result.finally(() => {
|
|
||||||
pageTrackingEnabled = previous;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
pageTrackingEnabled = previous;
|
|
||||||
return result;
|
|
||||||
} catch (err) {
|
|
||||||
pageTrackingEnabled = previous;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startPageTracking() {
|
|
||||||
pageTrackingEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function installPageResourceTracker() {
|
|
||||||
if (installedTracker) return;
|
|
||||||
installedTracker = true;
|
|
||||||
|
|
||||||
document.querySelectorAll("script[src]").forEach((script) => {
|
|
||||||
const src = script.getAttribute("src");
|
|
||||||
if (src && script.type !== "module") loadedClassicScripts.add(normalizeUrl(src));
|
|
||||||
});
|
|
||||||
|
|
||||||
original.addEventListener = EventTarget.prototype.addEventListener;
|
|
||||||
original.removeEventListener = EventTarget.prototype.removeEventListener;
|
|
||||||
original.setInterval = window.setInterval;
|
|
||||||
original.clearInterval = window.clearInterval;
|
|
||||||
original.WebSocket = window.WebSocket;
|
|
||||||
original.Worker = window.Worker;
|
|
||||||
|
|
||||||
EventTarget.prototype.addEventListener = function (type, listener, options) {
|
|
||||||
if (pageTrackingEnabled && listener) {
|
|
||||||
tracked.events.add({ target: this, type, listener, options });
|
|
||||||
}
|
|
||||||
return original.addEventListener.call(this, type, listener, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventTarget.prototype.removeEventListener = function (type, listener, options) {
|
|
||||||
for (const entry of tracked.events) {
|
|
||||||
if (entry.target === this && entry.type === type && entry.listener === listener) {
|
|
||||||
tracked.events.delete(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return original.removeEventListener.call(this, type, listener, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.setInterval = function (...args) {
|
|
||||||
const id = original.setInterval.apply(window, args);
|
|
||||||
if (pageTrackingEnabled) tracked.intervals.add(id);
|
|
||||||
return id;
|
|
||||||
};
|
|
||||||
|
|
||||||
window.clearInterval = function (id) {
|
|
||||||
tracked.intervals.delete(id);
|
|
||||||
return original.clearInterval.call(window, id);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof original.WebSocket === "function") {
|
|
||||||
window.WebSocket = function (...args) {
|
|
||||||
const socket = new original.WebSocket(...args);
|
|
||||||
if (pageTrackingEnabled) tracked.sockets.add(socket);
|
|
||||||
return socket;
|
|
||||||
};
|
|
||||||
window.WebSocket.prototype = original.WebSocket.prototype;
|
|
||||||
Object.defineProperty(window.WebSocket, "CONNECTING", { value: original.WebSocket.CONNECTING });
|
|
||||||
Object.defineProperty(window.WebSocket, "OPEN", { value: original.WebSocket.OPEN });
|
|
||||||
Object.defineProperty(window.WebSocket, "CLOSING", { value: original.WebSocket.CLOSING });
|
|
||||||
Object.defineProperty(window.WebSocket, "CLOSED", { value: original.WebSocket.CLOSED });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof original.Worker === "function") {
|
|
||||||
window.Worker = function (...args) {
|
|
||||||
const worker = new original.Worker(...args);
|
|
||||||
if (pageTrackingEnabled) tracked.workers.add(worker);
|
|
||||||
return worker;
|
|
||||||
};
|
|
||||||
window.Worker.prototype = original.Worker.prototype;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeHead(nextDoc, baseUrl) {
|
|
||||||
document.title = nextDoc.title || document.title;
|
|
||||||
|
|
||||||
const currentHtmlClasses = Array.from(document.documentElement.classList).filter((name) => (
|
|
||||||
name.startsWith("nav-pre") || name === "dark"
|
|
||||||
));
|
|
||||||
document.documentElement.className = nextDoc.documentElement.className || "";
|
|
||||||
currentHtmlClasses.forEach((name) => document.documentElement.classList.add(name));
|
|
||||||
|
|
||||||
nextDoc.head.querySelectorAll("link").forEach((link) => {
|
|
||||||
const href = link.getAttribute("href");
|
|
||||||
const rel = link.getAttribute("rel") || "";
|
|
||||||
if (!href) return;
|
|
||||||
const normalized = normalizeUrl(href, baseUrl);
|
|
||||||
const exists = Array.from(document.head.querySelectorAll("link")).some((existing) => (
|
|
||||||
(existing.getAttribute("rel") || "") === rel
|
|
||||||
&& normalizeUrl(existing.getAttribute("href") || "") === normalized
|
|
||||||
));
|
|
||||||
if (!exists) {
|
|
||||||
const clone = link.cloneNode(true);
|
|
||||||
clone.setAttribute("href", normalized);
|
|
||||||
document.head.appendChild(clone);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nextDoc.head.querySelectorAll("style").forEach((style) => {
|
|
||||||
const text = style.textContent || "";
|
|
||||||
const exists = Array.from(document.head.querySelectorAll("style[data-soft-nav-style]"))
|
|
||||||
.some((existing) => existing.textContent === text);
|
|
||||||
if (!exists) {
|
|
||||||
const clone = style.cloneNode(true);
|
|
||||||
clone.dataset.softNavStyle = "true";
|
|
||||||
document.head.appendChild(clone);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceBody(nextDoc) {
|
|
||||||
const preserved = Array.from(document.body.children).filter(isPersistentNode);
|
|
||||||
const nav = document.getElementById("top-nav");
|
|
||||||
const anchor = document.getElementById("cert-trust-banner")
|
|
||||||
|| document.getElementById("sb-mobile-bar")
|
|
||||||
|| nav;
|
|
||||||
const preservedClasses = Array.from(document.body.classList).filter((name) => (
|
|
||||||
name.startsWith("sb-") || name === "nav-hidden"
|
|
||||||
));
|
|
||||||
|
|
||||||
Array.from(document.body.children).forEach((node) => {
|
|
||||||
if (!preserved.includes(node)) node.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.className = nextDoc.body.className || "";
|
|
||||||
preservedClasses.forEach((name) => document.body.classList.add(name));
|
|
||||||
|
|
||||||
let insertAfter = anchor;
|
|
||||||
Array.from(nextDoc.body.children).forEach((node) => {
|
|
||||||
if (node.matches?.("script") || node.id === "top-nav") return;
|
|
||||||
const clone = document.importNode(node, true);
|
|
||||||
if (insertAfter?.parentNode) {
|
|
||||||
insertAfter.parentNode.insertBefore(clone, insertAfter.nextSibling);
|
|
||||||
insertAfter = clone;
|
|
||||||
} else {
|
|
||||||
document.body.appendChild(clone);
|
|
||||||
insertAfter = clone;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendScript(script, baseUrl) {
|
|
||||||
if (shouldSkipScript(script)) return Promise.resolve();
|
|
||||||
|
|
||||||
const src = script.getAttribute("src");
|
|
||||||
const type = script.getAttribute("type") || "";
|
|
||||||
if (src && type !== "module") {
|
|
||||||
const normalized = normalizeUrl(src, baseUrl);
|
|
||||||
if (loadedClassicScripts.has(normalized)) return Promise.resolve();
|
|
||||||
loadedClassicScripts.add(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const next = document.createElement("script");
|
|
||||||
for (const { name, value } of script.attributes) {
|
|
||||||
if (name !== "src") next.setAttribute(name, value);
|
|
||||||
}
|
|
||||||
next.dataset.softNavScript = "true";
|
|
||||||
next.onload = () => resolve();
|
|
||||||
next.onerror = () => reject(new Error(`Failed to load ${src || "inline script"}`));
|
|
||||||
|
|
||||||
if (src) {
|
|
||||||
const url = new URL(src, baseUrl);
|
|
||||||
if (type === "module" && url.origin === window.location.origin) {
|
|
||||||
url.searchParams.set("softNav", String(navSeq));
|
|
||||||
}
|
|
||||||
next.src = url.href;
|
|
||||||
} else {
|
|
||||||
next.textContent = script.textContent || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.appendChild(next);
|
|
||||||
if (!src) resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runPageScripts(nextDoc, baseUrl) {
|
|
||||||
const scripts = Array.from(nextDoc.querySelectorAll("script"));
|
|
||||||
for (const script of scripts) {
|
|
||||||
await appendScript(script, baseUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshGlobalEnhancements() {
|
|
||||||
if (typeof window.addRippleEffect === "function") {
|
|
||||||
document
|
|
||||||
.querySelectorAll("button:not(.no-ripple), .button:not(.no-ripple)")
|
|
||||||
.forEach((button) => window.addRippleEffect(button));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSoftNavigationCandidate(url, link) {
|
|
||||||
if (url.origin !== window.location.origin) return false;
|
|
||||||
if (link?.target && link.target !== "_self") return false;
|
|
||||||
if (link?.hasAttribute("download")) return false;
|
|
||||||
if (url.pathname.startsWith("/api/")) return false;
|
|
||||||
if (url.pathname.includes(".")) return false;
|
|
||||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupSoftNavigation(host, { onPathChange } = {}) {
|
|
||||||
if (!host || host.dataset.softNavReady === "true") return;
|
|
||||||
host.dataset.softNavReady = "true";
|
|
||||||
|
|
||||||
async function navigateTo(href, { replace = false } = {}) {
|
|
||||||
const url = new URL(href, window.location.href);
|
|
||||||
if (!isSoftNavigationCandidate(url)) {
|
|
||||||
window.location.href = url.href;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navSeq += 1;
|
|
||||||
document.body.classList.add("soft-nav-loading");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(url.href, { credentials: "include" });
|
|
||||||
if (!res.ok) throw new Error(`Navigation failed: ${res.status}`);
|
|
||||||
const html = await res.text();
|
|
||||||
const nextDoc = new DOMParser().parseFromString(html, "text/html");
|
|
||||||
if (!nextDoc.getElementById("top-nav")) {
|
|
||||||
window.location.href = url.href;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupTrackedResources();
|
|
||||||
mergeHead(nextDoc, url.href);
|
|
||||||
replaceBody(nextDoc);
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
if (replace) history.replaceState({}, "", url.href);
|
|
||||||
else history.pushState({}, "", url.href);
|
|
||||||
onPathChange?.(url.pathname);
|
|
||||||
await runPageScripts(nextDoc, url.href);
|
|
||||||
refreshGlobalEnhancements();
|
|
||||||
window.dispatchEvent(new CustomEvent("overlord:soft-nav", { detail: { path: url.pathname } }));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Soft navigation failed:", err);
|
|
||||||
window.location.href = url.href;
|
|
||||||
} finally {
|
|
||||||
document.body.classList.remove("soft-nav-loading");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
host.addEventListener("click", (event) => {
|
|
||||||
const link = event.target?.closest?.("a[href]");
|
|
||||||
if (!link) return;
|
|
||||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return;
|
|
||||||
|
|
||||||
const url = new URL(link.getAttribute("href"), window.location.href);
|
|
||||||
if (!isSoftNavigationCandidate(url, link)) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
navigateTo(url.href);
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
window.addEventListener("popstate", () => {
|
|
||||||
navigateTo(window.location.href, { replace: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
window.overlordSoftNavigate = navigateTo;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import { cache, config, visit } from "/vendor/hotwired/turbo.es2017-esm.js";
|
||||||
|
|
||||||
|
let installed = false;
|
||||||
|
let pageTrackingEnabled = false;
|
||||||
|
let visitSequence = 0;
|
||||||
|
let pendingVisitUrl = "";
|
||||||
|
|
||||||
|
const tracked = {
|
||||||
|
events: new Set(),
|
||||||
|
intervals: new Set(),
|
||||||
|
sockets: new Set(),
|
||||||
|
workers: new Set(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const original = {};
|
||||||
|
const loadedClassicScripts = new Set();
|
||||||
|
const persistentIds = [
|
||||||
|
"top-nav",
|
||||||
|
"sb-mobile-bar",
|
||||||
|
"sb-backdrop",
|
||||||
|
"nav-reveal-btn",
|
||||||
|
"chat-bubble",
|
||||||
|
"chat-panel",
|
||||||
|
"cert-trust-banner",
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeUrl(value, base = window.location.href) {
|
||||||
|
try {
|
||||||
|
return new URL(value, base).href;
|
||||||
|
} catch {
|
||||||
|
return value || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scriptPath(value) {
|
||||||
|
try {
|
||||||
|
return new URL(value, window.location.href).pathname;
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupPageResources() {
|
||||||
|
if (!pageTrackingEnabled) return;
|
||||||
|
pageTrackingEnabled = false;
|
||||||
|
window.dispatchEvent(new Event("pagehide"));
|
||||||
|
|
||||||
|
for (const id of tracked.intervals) original.clearInterval(id);
|
||||||
|
for (const entry of tracked.events) {
|
||||||
|
original.removeEventListener.call(entry.target, entry.type, entry.listener, entry.options);
|
||||||
|
}
|
||||||
|
for (const socket of tracked.sockets) {
|
||||||
|
try {
|
||||||
|
socket.onopen = null;
|
||||||
|
socket.onmessage = null;
|
||||||
|
socket.onerror = null;
|
||||||
|
socket.onclose = null;
|
||||||
|
socket.close(1000, "Turbo navigation");
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
for (const worker of tracked.workers) {
|
||||||
|
try {
|
||||||
|
worker.terminate();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracked.events.clear();
|
||||||
|
tracked.intervals.clear();
|
||||||
|
tracked.sockets.clear();
|
||||||
|
tracked.workers.clear();
|
||||||
|
document.getElementById("cmdp-root")?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runWithoutPageTracking(fn) {
|
||||||
|
const previous = pageTrackingEnabled;
|
||||||
|
pageTrackingEnabled = false;
|
||||||
|
try {
|
||||||
|
const result = fn();
|
||||||
|
if (result && typeof result.then === "function") {
|
||||||
|
return result.finally(() => {
|
||||||
|
pageTrackingEnabled = previous;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pageTrackingEnabled = previous;
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
pageTrackingEnabled = previous;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installPageResourceTracker() {
|
||||||
|
if (original.addEventListener) return;
|
||||||
|
|
||||||
|
document.querySelectorAll("script[src]").forEach((script) => {
|
||||||
|
const src = script.getAttribute("src");
|
||||||
|
if (src && script.type !== "module") loadedClassicScripts.add(normalizeUrl(src));
|
||||||
|
});
|
||||||
|
|
||||||
|
original.addEventListener = EventTarget.prototype.addEventListener;
|
||||||
|
original.removeEventListener = EventTarget.prototype.removeEventListener;
|
||||||
|
original.setInterval = window.setInterval;
|
||||||
|
original.clearInterval = window.clearInterval;
|
||||||
|
original.WebSocket = window.WebSocket;
|
||||||
|
original.Worker = window.Worker;
|
||||||
|
|
||||||
|
EventTarget.prototype.addEventListener = function (type, listener, options) {
|
||||||
|
if (pageTrackingEnabled && listener) {
|
||||||
|
tracked.events.add({ target: this, type, listener, options });
|
||||||
|
}
|
||||||
|
return original.addEventListener.call(this, type, listener, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
EventTarget.prototype.removeEventListener = function (type, listener, options) {
|
||||||
|
for (const entry of tracked.events) {
|
||||||
|
if (entry.target === this && entry.type === type && entry.listener === listener) {
|
||||||
|
tracked.events.delete(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return original.removeEventListener.call(this, type, listener, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.setInterval = function (...args) {
|
||||||
|
const id = original.setInterval.apply(window, args);
|
||||||
|
if (pageTrackingEnabled) tracked.intervals.add(id);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearInterval = function (id) {
|
||||||
|
tracked.intervals.delete(id);
|
||||||
|
return original.clearInterval.call(window, id);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof original.WebSocket === "function") {
|
||||||
|
window.WebSocket = function (...args) {
|
||||||
|
const socket = new original.WebSocket(...args);
|
||||||
|
if (pageTrackingEnabled) tracked.sockets.add(socket);
|
||||||
|
return socket;
|
||||||
|
};
|
||||||
|
window.WebSocket.prototype = original.WebSocket.prototype;
|
||||||
|
Object.defineProperties(window.WebSocket, {
|
||||||
|
CONNECTING: { value: original.WebSocket.CONNECTING },
|
||||||
|
OPEN: { value: original.WebSocket.OPEN },
|
||||||
|
CLOSING: { value: original.WebSocket.CLOSING },
|
||||||
|
CLOSED: { value: original.WebSocket.CLOSED },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof original.Worker === "function") {
|
||||||
|
window.Worker = function (...args) {
|
||||||
|
const worker = new original.Worker(...args);
|
||||||
|
if (pageTrackingEnabled) tracked.workers.add(worker);
|
||||||
|
return worker;
|
||||||
|
};
|
||||||
|
window.Worker.prototype = original.Worker.prototype;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preserveGlobalElements(newBody) {
|
||||||
|
for (const id of persistentIds) {
|
||||||
|
const current = document.getElementById(id);
|
||||||
|
if (!current) continue;
|
||||||
|
|
||||||
|
current.setAttribute("data-turbo-permanent", "");
|
||||||
|
let incoming = newBody.querySelector(`#${CSS.escape(id)}`);
|
||||||
|
if (!incoming) {
|
||||||
|
incoming = document.createElement(current.tagName.toLowerCase());
|
||||||
|
incoming.id = id;
|
||||||
|
newBody.appendChild(incoming);
|
||||||
|
}
|
||||||
|
incoming.setAttribute("data-turbo-permanent", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preserveLayoutClasses(newBody) {
|
||||||
|
for (const name of document.body.classList) {
|
||||||
|
if (name.startsWith("sb-") || name === "nav-hidden") {
|
||||||
|
newBody.classList.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareIncomingScripts(newBody) {
|
||||||
|
visitSequence += 1;
|
||||||
|
|
||||||
|
for (const script of newBody.querySelectorAll("script")) {
|
||||||
|
const src = script.getAttribute("src");
|
||||||
|
if (!src) continue;
|
||||||
|
|
||||||
|
const path = scriptPath(src);
|
||||||
|
if (path === "/assets/nav.js" || path === "/assets/nav-prelude.js") {
|
||||||
|
script.setAttribute("data-turbo-eval", "false");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (script.type === "module") {
|
||||||
|
const url = new URL(src, pendingVisitUrl || window.location.href);
|
||||||
|
if (url.origin === window.location.origin) {
|
||||||
|
url.searchParams.set("turboVisit", String(visitSequence));
|
||||||
|
script.src = url.href;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizeUrl(src, pendingVisitUrl || window.location.href);
|
||||||
|
if (loadedClassicScripts.has(normalized)) {
|
||||||
|
script.setAttribute("data-turbo-eval", "false");
|
||||||
|
} else {
|
||||||
|
loadedClassicScripts.add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshGlobalEnhancements() {
|
||||||
|
if (typeof window.addRippleEffect === "function") {
|
||||||
|
document
|
||||||
|
.querySelectorAll("button:not(.no-ripple), .button:not(.no-ripple)")
|
||||||
|
.forEach((button) => window.addRippleEffect(button));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupTurboNavigation({ onPathChange } = {}) {
|
||||||
|
if (installed) return;
|
||||||
|
installed = true;
|
||||||
|
|
||||||
|
installPageResourceTracker();
|
||||||
|
config.drive.progressBarDelay = 150;
|
||||||
|
// Existing forms already have explicit fetch handlers. Turbo owns navigation only.
|
||||||
|
config.forms.mode = "off";
|
||||||
|
cache.exemptPageFromCache();
|
||||||
|
|
||||||
|
document.getElementById("top-nav")?.setAttribute("data-turbo-permanent", "");
|
||||||
|
|
||||||
|
document.addEventListener("turbo:before-visit", (event) => {
|
||||||
|
pendingVisitUrl = event.detail.url || "";
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("turbo:before-render", (event) => {
|
||||||
|
const newBody = event.detail.newBody;
|
||||||
|
if (!newBody?.querySelector("#top-nav")) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.assign(pendingVisitUrl || window.location.href);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupPageResources();
|
||||||
|
preserveGlobalElements(newBody);
|
||||||
|
preserveLayoutClasses(newBody);
|
||||||
|
prepareIncomingScripts(newBody);
|
||||||
|
pageTrackingEnabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("turbo:load", () => {
|
||||||
|
cache.exemptPageFromCache();
|
||||||
|
pendingVisitUrl = "";
|
||||||
|
pageTrackingEnabled = true;
|
||||||
|
onPathChange?.(window.location.pathname);
|
||||||
|
refreshGlobalEnhancements();
|
||||||
|
window.dispatchEvent(new CustomEvent("overlord:turbo-load", {
|
||||||
|
detail: { path: window.location.pathname },
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
pageTrackingEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function turboVisit(url, options) {
|
||||||
|
visit(url, options);
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
<link rel="stylesheet" href="/assets/main.css" />
|
<link rel="stylesheet" href="/assets/main.css" />
|
||||||
<link rel="stylesheet" href="/assets/custom.css" />
|
<link rel="stylesheet" href="/assets/custom.css" />
|
||||||
<script defer src="/vendor/cytoscape/cytoscape.min.js"></script>
|
<script defer src="/vendor/cytoscape/cytoscape.min.js"></script>
|
||||||
<script defer type="module" src="/assets/client-graph.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-slate-950 text-slate-100">
|
<body class="min-h-screen bg-slate-950 text-slate-100">
|
||||||
<header id="top-nav"></header>
|
<header id="top-nav"></header>
|
||||||
@@ -64,5 +63,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<script type="module" src="/assets/nav.js"></script>
|
<script type="module" src="/assets/nav.js"></script>
|
||||||
|
<script type="module" src="/assets/client-graph.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1090,7 +1090,7 @@
|
|||||||
<button id="reg-key-generate-btn" type="button" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-emerald-700 hover:bg-emerald-600 transition-colors text-white text-sm font-medium">
|
<button id="reg-key-generate-btn" type="button" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-emerald-700 hover:bg-emerald-600 transition-colors text-white text-sm font-medium">
|
||||||
<i class="fa-solid fa-plus"></i>Generate
|
<i class="fa-solid fa-plus"></i>Generate
|
||||||
</button>
|
</button>
|
||||||
<a id="reg-key-export-btn" href="/api/registration/keys/export" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors text-white text-sm font-medium">
|
<a id="reg-key-export-btn" href="/api/registration/keys/export" download data-turbo="false" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors text-white text-sm font-medium">
|
||||||
<i class="fa-solid fa-file-export"></i>Export
|
<i class="fa-solid fa-file-export"></i>Export
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,6 +197,14 @@ copyFile(
|
|||||||
path.join(VENDOR, "cytoscape", "cytoscape.min.js"),
|
path.join(VENDOR, "cytoscape", "cytoscape.min.js"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* ── Hotwire Turbo ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
console.log("Copying Hotwire Turbo ...");
|
||||||
|
copyFile(
|
||||||
|
path.join(NM, "@hotwired", "turbo", "dist", "turbo.es2017-esm.js"),
|
||||||
|
path.join(VENDOR, "hotwired", "turbo.es2017-esm.js"),
|
||||||
|
);
|
||||||
|
|
||||||
/* ── highlight.js (bundle core + languages) ──────────────────────── */
|
/* ── highlight.js (bundle core + languages) ──────────────────────── */
|
||||||
|
|
||||||
console.log("Bundling highlight.js ...");
|
console.log("Bundling highlight.js ...");
|
||||||
|
|||||||
Reference in New Issue
Block a user