refactor: convert shared UI core to TypeScript
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
"use strict";
|
||||
const browserWindow = window;
|
||||
const preparedTabLists = /* @__PURE__ */ new WeakSet();
|
||||
function elementById(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) throw new Error(`Missing required UI element #${id}`);
|
||||
return element;
|
||||
}
|
||||
(function initUiCore() {
|
||||
const api = window.trxUi = window.trxUi || {};
|
||||
const api = browserWindow.trxUi ?? {};
|
||||
browserWindow.trxUi = api;
|
||||
function ensureLiveRegions() {
|
||||
if (!document.getElementById("toast-region")) {
|
||||
const region = document.createElement("div");
|
||||
@@ -45,21 +53,28 @@
|
||||
});
|
||||
toast.appendChild(button);
|
||||
}
|
||||
document.getElementById("toast-region").appendChild(toast);
|
||||
requestAnimationFrame(() => toast.classList.add("toast-visible"));
|
||||
if (duration > 0) setTimeout(() => toast.remove(), duration);
|
||||
elementById("toast-region").appendChild(toast);
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add("toast-visible");
|
||||
});
|
||||
if (duration > 0) setTimeout(() => {
|
||||
toast.remove();
|
||||
}, duration);
|
||||
return toast;
|
||||
};
|
||||
api.confirm = function confirmAction(options = {}) {
|
||||
ensureLiveRegions();
|
||||
const dialog = document.getElementById("ui-confirm-dialog");
|
||||
document.getElementById("ui-confirm-title").textContent = options.title || "Confirm action";
|
||||
document.getElementById("ui-confirm-message").textContent = options.message || "Continue?";
|
||||
const dialog = elementById("ui-confirm-dialog");
|
||||
elementById("ui-confirm-title").textContent = options.title || "Confirm action";
|
||||
elementById("ui-confirm-message").textContent = options.message || "Continue?";
|
||||
const confirmButton = dialog.querySelector('[value="confirm"]');
|
||||
if (!confirmButton) throw new Error("Confirmation dialog has no confirm button");
|
||||
confirmButton.textContent = options.confirmLabel || "Confirm";
|
||||
confirmButton.classList.toggle("danger", options.danger !== false);
|
||||
return new Promise((resolve) => {
|
||||
const finish = () => resolve(dialog.returnValue === "confirm");
|
||||
const finish = () => {
|
||||
resolve(dialog.returnValue === "confirm");
|
||||
};
|
||||
dialog.addEventListener("close", finish, { once: true });
|
||||
dialog.showModal();
|
||||
});
|
||||
@@ -77,8 +92,8 @@
|
||||
};
|
||||
api.prepareTabList = function prepareTabList(bar, kind = "primary") {
|
||||
if (!bar) return;
|
||||
if (bar._accessibleTabsPrepared) return;
|
||||
bar._accessibleTabsPrepared = true;
|
||||
if (preparedTabLists.has(bar)) return;
|
||||
preparedTabLists.add(bar);
|
||||
const selector = kind === "primary" ? ".tab[data-tab]" : ".sub-tab[data-subtab]";
|
||||
const buttons = Array.from(bar.querySelectorAll(selector));
|
||||
bar.setAttribute("role", "tablist");
|
||||
@@ -87,6 +102,7 @@
|
||||
button.setAttribute("aria-selected", String(button.classList.contains("active")));
|
||||
button.tabIndex = button.classList.contains("active") || !buttons.some((b) => b.classList.contains("active")) && index === 0 ? 0 : -1;
|
||||
const key = button.dataset.tab || button.dataset.subtab;
|
||||
if (!key) return;
|
||||
button.setAttribute("aria-controls", `${kind === "primary" ? "tab-" : "subtab-"}${key}`);
|
||||
const panel = document.getElementById(`${kind === "primary" ? "tab-" : "subtab-"}${key}`);
|
||||
if (panel) {
|
||||
@@ -96,11 +112,12 @@
|
||||
}
|
||||
});
|
||||
bar.addEventListener("keydown", (event) => {
|
||||
if (!buttons.includes(event.target)) return;
|
||||
if (!(event.target instanceof HTMLElement) || !buttons.includes(event.target)) return;
|
||||
const direction = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : event.key === "ArrowLeft" || event.key === "ArrowUp" ? -1 : 0;
|
||||
if (!direction) return;
|
||||
event.preventDefault();
|
||||
const next = buttons[(buttons.indexOf(event.target) + direction + buttons.length) % buttons.length];
|
||||
if (!next) return;
|
||||
next.focus();
|
||||
next.click();
|
||||
});
|
||||
@@ -128,7 +145,7 @@
|
||||
return localStorage.getItem(layoutStorageKey()) || localStorage.getItem("trxOperatorLayout") || "compact";
|
||||
}
|
||||
function layoutAvailable(layout) {
|
||||
return !layout.capability || layoutCapabilities[layout.capability] === true;
|
||||
return !layout.capability || layoutCapabilities[layout.capability];
|
||||
}
|
||||
function unavailableLayoutMessage() {
|
||||
const unavailable = Object.values(layouts).filter((layout) => !layoutAvailable(layout) && layout.unavailable);
|
||||
@@ -168,19 +185,20 @@
|
||||
api.applyLayout(select?.value || saved, { persist: false });
|
||||
};
|
||||
api.applyLayout = function applyLayout(name, options = {}) {
|
||||
const requestedLayout = layouts[name];
|
||||
const permittedName = requestedLayout && layoutAvailable(requestedLayout) ? name : "compact";
|
||||
const requestedName = name in layouts ? name : "compact";
|
||||
const requestedLayout = layouts[requestedName];
|
||||
const permittedName = layoutAvailable(requestedLayout) ? requestedName : "compact";
|
||||
const layout = layouts[permittedName];
|
||||
document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact";
|
||||
if (options.persist !== false) localStorage.setItem(layoutStorageKey(), document.body.dataset.operatorLayout);
|
||||
if (options.persist !== false) localStorage.setItem(layoutStorageKey(), permittedName);
|
||||
const details = document.getElementById("advanced-radio-controls");
|
||||
if (details) details.open = layout.advanced;
|
||||
const audioDetails = document.getElementById("audio-controls");
|
||||
if (audioDetails) audioDetails.open = layout.audio;
|
||||
const schedulerDetails = document.getElementById("scheduler-controls");
|
||||
if (schedulerDetails) schedulerDetails.open = layout.scheduler;
|
||||
if (options.navigate && typeof window.navigateToTab === "function") {
|
||||
window.navigateToTab(layout.preferredTab);
|
||||
if (options.navigate && typeof browserWindow.navigateToTab === "function") {
|
||||
browserWindow.navigateToTab(layout.preferredTab);
|
||||
}
|
||||
};
|
||||
function installLayoutControls() {
|
||||
@@ -190,12 +208,15 @@
|
||||
label.className = "operator-layout-picker";
|
||||
label.innerHTML = '<span class="visually-hidden">Operator layout</span><select id="operator-layout-select" aria-label="Operator layout"></select>';
|
||||
const select = label.querySelector("select");
|
||||
if (!select) throw new Error("Operator layout picker has no select element");
|
||||
const savedLayout = savedLayoutName();
|
||||
actions.insertBefore(label, actions.firstChild);
|
||||
select.value = savedLayout;
|
||||
refreshLayoutOptions();
|
||||
if (savedLayout !== "broadcast" && layouts[savedLayout]) select.value = savedLayout;
|
||||
select.addEventListener("change", () => api.applyLayout(select.value, { navigate: true }));
|
||||
if (savedLayout !== "broadcast" && savedLayout in layouts) select.value = savedLayout;
|
||||
select.addEventListener("change", () => {
|
||||
api.applyLayout(select.value, { navigate: true });
|
||||
});
|
||||
api.applyLayout(select.value);
|
||||
}
|
||||
const tray = document.querySelector(".controls-tray");
|
||||
@@ -205,6 +226,7 @@
|
||||
details.className = "advanced-radio-controls";
|
||||
details.innerHTML = '<summary>Advanced radio controls</summary><div class="advanced-radio-body"></div>';
|
||||
const body = details.querySelector(".advanced-radio-body");
|
||||
if (!body) throw new Error("Advanced controls have no body");
|
||||
["sdr-settings-row", "vchan-row", "tx-limit-row"].forEach((id) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) body.appendChild(element);
|
||||
@@ -244,7 +266,7 @@
|
||||
item.dataset.navigateTab = tabName;
|
||||
item.textContent = source.textContent.trim();
|
||||
item.addEventListener("click", () => {
|
||||
if (typeof window.navigateToTab === "function") window.navigateToTab(tabName);
|
||||
if (typeof browserWindow.navigateToTab === "function") browserWindow.navigateToTab(tabName);
|
||||
closeMore();
|
||||
});
|
||||
menu.appendChild(item);
|
||||
@@ -255,13 +277,17 @@
|
||||
if (open) menu.querySelector('[role="menuitem"]')?.focus();
|
||||
});
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!menu.contains(event.target) && !more.contains(event.target)) closeMore();
|
||||
if (!(event.target instanceof Node) || !menu.contains(event.target) && !more.contains(event.target)) closeMore();
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") closeMore(true);
|
||||
});
|
||||
window.addEventListener("resize", () => closeMore());
|
||||
window.addEventListener("popstate", () => closeMore());
|
||||
window.addEventListener("resize", () => {
|
||||
closeMore();
|
||||
});
|
||||
window.addEventListener("popstate", () => {
|
||||
closeMore();
|
||||
});
|
||||
nav.append(more, menu);
|
||||
}
|
||||
function installDecoderPicker() {
|
||||
@@ -294,6 +320,7 @@
|
||||
if (!bar) return;
|
||||
bar.querySelectorAll(".sub-tab[data-subtab]").forEach((button) => {
|
||||
const id = button.dataset.subtab;
|
||||
if (!id) return;
|
||||
if (id === "overview" || button.querySelector(".decoder-state-dot")) return;
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "decoder-state-dot";
|
||||
@@ -318,12 +345,16 @@
|
||||
installDecoderPicker();
|
||||
installDecoderBadges();
|
||||
api.prepareTabList(document.querySelector(".tab-bar-nav"), "primary");
|
||||
document.querySelectorAll(".sub-tab-bar").forEach((bar) => api.prepareTabList(bar, "secondary"));
|
||||
document.querySelectorAll(".sub-tab-bar").forEach((bar) => {
|
||||
api.prepareTabList(bar, "secondary");
|
||||
});
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
const message = event.reason?.message || "An operation failed unexpectedly";
|
||||
const message = event.reason instanceof Error ? event.reason.message : "An operation failed unexpectedly";
|
||||
api.notify(message, { kind: "error" });
|
||||
});
|
||||
};
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", api.init, { once: true });
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => {
|
||||
api.init();
|
||||
}, { once: true });
|
||||
else api.init();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user