Complete TypeScript frontend migration #22
@@ -80,7 +80,7 @@
|
||||
if (decoderRegistry.length > 0) callback();
|
||||
else readyCallbacks.push(callback);
|
||||
}
|
||||
function hideUnsupportedDecoderUi() {
|
||||
function applyDecoderRegistryVisibility() {
|
||||
const knownIds = new Set(decoderRegistry.map(({ id }) => id));
|
||||
const alwaysShow = /* @__PURE__ */ new Set(["overview", "rds", "sat"]);
|
||||
document.querySelectorAll(
|
||||
@@ -128,7 +128,7 @@
|
||||
readyCallbacks.splice(0).forEach((callback) => {
|
||||
callback();
|
||||
});
|
||||
hideUnsupportedDecoderUi();
|
||||
applyDecoderRegistryVisibility();
|
||||
onLoaded();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch decoder registry:", error);
|
||||
@@ -352,6 +352,44 @@
|
||||
return value;
|
||||
}
|
||||
|
||||
// src/features/navigation/routes.ts
|
||||
var TAB_ORDER = [
|
||||
"main",
|
||||
"bookmarks",
|
||||
"digital-modes",
|
||||
"map",
|
||||
"statistics",
|
||||
"recorder",
|
||||
"settings",
|
||||
"about"
|
||||
];
|
||||
var TAB_PATHS = {
|
||||
main: "/",
|
||||
bookmarks: "/bookmarks",
|
||||
"digital-modes": "/digital-modes",
|
||||
map: "/map",
|
||||
statistics: "/statistics",
|
||||
recorder: "/recorder",
|
||||
settings: "/settings",
|
||||
about: "/about"
|
||||
};
|
||||
function normalizeTabPath(pathname) {
|
||||
const raw = pathname.length > 0 ? pathname : "/";
|
||||
return raw === "/" ? "/" : raw.replace(/\/+$/, "") || "/";
|
||||
}
|
||||
function tabFromPath(pathname) {
|
||||
const normalized = normalizeTabPath(pathname);
|
||||
const match = Object.entries(TAB_PATHS).find(([, path]) => path === normalized);
|
||||
return match ? match[0] : "main";
|
||||
}
|
||||
function updateTabHistory(name, replace = false) {
|
||||
const targetPath = TAB_PATHS[name];
|
||||
if (normalizeTabPath(window.location.pathname) === targetPath) return;
|
||||
const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`;
|
||||
if (replace) window.history.replaceState({}, "", nextUrl);
|
||||
else window.history.pushState({}, "", nextUrl);
|
||||
}
|
||||
|
||||
// src/app.js
|
||||
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
||||
var authRole = null;
|
||||
@@ -420,7 +458,7 @@
|
||||
document.querySelectorAll(".tab-bar .tab").forEach((btn) => {
|
||||
btn.classList.remove("active");
|
||||
});
|
||||
navigateToTab(tabFromPath(), { updateHistory: false, replaceHistory: true });
|
||||
navigateToTab(tabFromPath2(), { updateHistory: false, replaceHistory: true });
|
||||
syncTopBarAccess();
|
||||
}
|
||||
function showAuthError(msg) {
|
||||
@@ -4184,34 +4222,8 @@ ${unsupportedBandSummary()}`;
|
||||
});
|
||||
}
|
||||
var _activeTab = "main";
|
||||
var TAB_ORDER = ["main", "bookmarks", "digital-modes", "map", "statistics", "recorder", "settings", "about"];
|
||||
var TAB_PATHS = {
|
||||
main: "/",
|
||||
bookmarks: "/bookmarks",
|
||||
"digital-modes": "/digital-modes",
|
||||
map: "/map",
|
||||
recorder: "/recorder",
|
||||
settings: "/settings",
|
||||
about: "/about"
|
||||
};
|
||||
function normalizeTabPath(pathname) {
|
||||
const raw = typeof pathname === "string" && pathname.length > 0 ? pathname : "/";
|
||||
if (raw === "/") return "/";
|
||||
return raw.replace(/\/+$/, "") || "/";
|
||||
}
|
||||
function tabFromPath(pathname = window.location.pathname) {
|
||||
const normalized = normalizeTabPath(pathname);
|
||||
for (const [tabName, tabPath] of Object.entries(TAB_PATHS)) {
|
||||
if (normalized === tabPath) return tabName;
|
||||
}
|
||||
return "main";
|
||||
}
|
||||
function updateTabHistory(name, replaceHistory = false) {
|
||||
const targetPath = TAB_PATHS[name] || "/";
|
||||
if (normalizeTabPath(window.location.pathname) === targetPath) return;
|
||||
const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`;
|
||||
const method = replaceHistory ? "replaceState" : "pushState";
|
||||
window.history[method]({}, "", nextUrl);
|
||||
function tabFromPath2(pathname = window.location.pathname) {
|
||||
return tabFromPath(pathname);
|
||||
}
|
||||
var _mapInitTimer = null;
|
||||
function _initMapWhenReady() {
|
||||
@@ -4265,7 +4277,7 @@ ${unsupportedBandSummary()}`;
|
||||
panel.appendChild(tmpl.content.cloneNode(true));
|
||||
tmpl.remove();
|
||||
panel.querySelectorAll(".sub-tab-bar").forEach(_wireSubTabBar);
|
||||
if (decoderRegistry.length) hideUnsupportedDecoderTabs();
|
||||
if (decoderRegistry.length) applyDecoderRegistryVisibility();
|
||||
}
|
||||
if (updateHistory) {
|
||||
updateTabHistory(name, replaceHistory);
|
||||
@@ -4289,7 +4301,7 @@ ${unsupportedBandSummary()}`;
|
||||
navigateToTab(btn.dataset.tab);
|
||||
});
|
||||
window.addEventListener("popstate", () => {
|
||||
navigateToTab(tabFromPath(), { updateHistory: false });
|
||||
navigateToTab(tabFromPath2(), { updateHistory: false });
|
||||
});
|
||||
(function() {
|
||||
let tx = 0, ty = 0;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { escapeHtml as escapeMapHtml } from "./core/dom.js";
|
||||
import { loadSetting, saveSetting } from "./core/settings.js";
|
||||
import {
|
||||
applyDecoderRegistryVisibility,
|
||||
decoderRegistry,
|
||||
loadDecoderRegistry,
|
||||
} from "./core/decoder-registry.js";
|
||||
@@ -30,6 +31,11 @@ import {
|
||||
parseFrequencyInput,
|
||||
} from "./core/format.js";
|
||||
import { decodeCbor as decodeCborPayload } from "./core/cbor.js";
|
||||
import {
|
||||
TAB_ORDER,
|
||||
tabFromPath as tabFromPathname,
|
||||
updateTabHistory,
|
||||
} from "./features/navigation/routes.js";
|
||||
|
||||
// --- Decoder registry (fetched from /decoders on load) ---
|
||||
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
||||
@@ -4258,37 +4264,8 @@ if (spectrumBwSweetBtn) {
|
||||
|
||||
// --- Tab navigation ---
|
||||
let _activeTab = "main"; // tracked for render-path tab awareness
|
||||
const TAB_ORDER = ["main", "bookmarks", "digital-modes", "map", "statistics", "recorder", "settings", "about"];
|
||||
const TAB_PATHS = {
|
||||
main: "/",
|
||||
bookmarks: "/bookmarks",
|
||||
"digital-modes": "/digital-modes",
|
||||
map: "/map",
|
||||
recorder: "/recorder",
|
||||
settings: "/settings",
|
||||
about: "/about",
|
||||
};
|
||||
|
||||
function normalizeTabPath(pathname) {
|
||||
const raw = typeof pathname === "string" && pathname.length > 0 ? pathname : "/";
|
||||
if (raw === "/") return "/";
|
||||
return raw.replace(/\/+$/, "") || "/";
|
||||
}
|
||||
|
||||
function tabFromPath(pathname = window.location.pathname) {
|
||||
const normalized = normalizeTabPath(pathname);
|
||||
for (const [tabName, tabPath] of Object.entries(TAB_PATHS)) {
|
||||
if (normalized === tabPath) return tabName;
|
||||
}
|
||||
return "main";
|
||||
}
|
||||
|
||||
function updateTabHistory(name, replaceHistory = false) {
|
||||
const targetPath = TAB_PATHS[name] || "/";
|
||||
if (normalizeTabPath(window.location.pathname) === targetPath) return;
|
||||
const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`;
|
||||
const method = replaceHistory ? "replaceState" : "pushState";
|
||||
window.history[method]({}, "", nextUrl);
|
||||
return tabFromPathname(pathname);
|
||||
}
|
||||
|
||||
// Initialise the Leaflet map, waiting for both Leaflet (L) and map-core.js
|
||||
@@ -4347,7 +4324,7 @@ function navigateToTab(name, options = {}) {
|
||||
// Wire sub-tab bars inside the freshly cloned content.
|
||||
panel.querySelectorAll(".sub-tab-bar").forEach(_wireSubTabBar);
|
||||
// Re-run decoder visibility for about-tab elements now in the DOM.
|
||||
if (decoderRegistry.length) hideUnsupportedDecoderTabs();
|
||||
if (decoderRegistry.length) applyDecoderRegistryVisibility();
|
||||
}
|
||||
if (updateHistory) {
|
||||
updateTabHistory(name, replaceHistory);
|
||||
|
||||
@@ -26,7 +26,7 @@ export function onDecoderRegistryReady(callback: () => void): void {
|
||||
else readyCallbacks.push(callback);
|
||||
}
|
||||
|
||||
function hideUnsupportedDecoderUi(): void {
|
||||
export function applyDecoderRegistryVisibility(): void {
|
||||
const knownIds = new Set(decoderRegistry.map(({ id }) => id));
|
||||
const alwaysShow = new Set(["overview", "rds", "sat"]);
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function loadDecoderRegistry(onLoaded: () => void): Promise<void> {
|
||||
decoderRegistry = decodeRegistry(await response.json());
|
||||
bridge.decoderRegistry = decoderRegistry;
|
||||
readyCallbacks.splice(0).forEach((callback) => { callback(); });
|
||||
hideUnsupportedDecoderUi();
|
||||
applyDecoderRegistryVisibility();
|
||||
onLoaded();
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to fetch decoder registry:", error);
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
export const TAB_ORDER = [
|
||||
"main", "bookmarks", "digital-modes", "map", "statistics", "recorder", "settings", "about",
|
||||
] as const;
|
||||
|
||||
export type TabName = typeof TAB_ORDER[number];
|
||||
|
||||
export const TAB_PATHS: Readonly<Record<TabName, string>> = {
|
||||
main: "/",
|
||||
bookmarks: "/bookmarks",
|
||||
"digital-modes": "/digital-modes",
|
||||
map: "/map",
|
||||
statistics: "/statistics",
|
||||
recorder: "/recorder",
|
||||
settings: "/settings",
|
||||
about: "/about",
|
||||
};
|
||||
|
||||
export function normalizeTabPath(pathname: string): string {
|
||||
const raw = pathname.length > 0 ? pathname : "/";
|
||||
return raw === "/" ? "/" : raw.replace(/\/+$/, "") || "/";
|
||||
}
|
||||
|
||||
export function tabFromPath(pathname: string): TabName {
|
||||
const normalized = normalizeTabPath(pathname);
|
||||
const match = Object.entries(TAB_PATHS).find(([, path]) => path === normalized);
|
||||
return match ? match[0] as TabName : "main";
|
||||
}
|
||||
|
||||
export function updateTabHistory(name: TabName, replace = false): void {
|
||||
const targetPath = TAB_PATHS[name];
|
||||
if (normalizeTabPath(window.location.pathname) === targetPath) return;
|
||||
const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`;
|
||||
if (replace) window.history.replaceState({}, "", nextUrl);
|
||||
else window.history.pushState({}, "", nextUrl);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
import { build } from "esbuild";
|
||||
|
||||
async function loadRoutes(window) {
|
||||
const result = await build({
|
||||
entryPoints: [new URL("../src/features/navigation/routes.ts", import.meta.url).pathname],
|
||||
bundle: true,
|
||||
format: "cjs",
|
||||
platform: "browser",
|
||||
target: "es2022",
|
||||
write: false,
|
||||
});
|
||||
const module = { exports: {} };
|
||||
vm.runInNewContext(result.outputFiles[0].text, { module, exports: module.exports, window });
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
test("all top-level tabs have stable round-trip routes", async () => {
|
||||
const calls = [];
|
||||
const window = {
|
||||
location: { pathname: "/", search: "?rig=one", hash: "#panel" },
|
||||
history: {
|
||||
pushState: (...args) => calls.push(["push", ...args]),
|
||||
replaceState: (...args) => calls.push(["replace", ...args]),
|
||||
},
|
||||
};
|
||||
const routes = await loadRoutes(window);
|
||||
for (const name of routes.TAB_ORDER) {
|
||||
assert.equal(routes.tabFromPath(routes.TAB_PATHS[name]), name);
|
||||
}
|
||||
routes.updateTabHistory("statistics");
|
||||
assert.equal(calls[0][0], "push");
|
||||
assert.equal(calls[0][3], "/statistics?rig=one#panel");
|
||||
});
|
||||
Reference in New Issue
Block a user