This commit is contained in:
268
www/nav.js
Normal file
268
www/nav.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* MiR-style 3-column navigation: primary rail + flyout submenu + content.
|
||||
*/
|
||||
(function () {
|
||||
const STORAGE_MODULE = "mirNavModule";
|
||||
const STORAGE_SECTION = "mirNavSection";
|
||||
const STORAGE_FLYOUT = "mirNavFlyoutOpen";
|
||||
|
||||
const MODULES = {
|
||||
dashboards: {
|
||||
items: [{ section: "dashboard", page: "dashboard" }],
|
||||
},
|
||||
setup: {
|
||||
items: [
|
||||
{ section: "missions", page: "missions" },
|
||||
{ section: "maps", page: "config" },
|
||||
],
|
||||
},
|
||||
monitoring: {
|
||||
items: [{ section: "monitoring-log", page: "monitoring" }],
|
||||
},
|
||||
system: {
|
||||
items: [{ section: "integrations", page: "integrations" }],
|
||||
},
|
||||
help: {
|
||||
items: [{ section: "help-api", page: "help" }],
|
||||
},
|
||||
};
|
||||
|
||||
const PAGE_NAV = {
|
||||
dashboard: { module: "dashboards", section: "dashboard" },
|
||||
config: { module: "setup", section: "maps" },
|
||||
missions: { module: "setup", section: "missions" },
|
||||
integrations: { module: "system", section: "integrations" },
|
||||
monitoring: { module: "monitoring", section: "monitoring-log" },
|
||||
help: { module: "help", section: "help-api" },
|
||||
};
|
||||
|
||||
let activeModule = "setup";
|
||||
let activeSection = "maps";
|
||||
let flyoutOpen = true;
|
||||
|
||||
const shellEl = () => document.getElementById("mirNavShell");
|
||||
const flyoutListEl = () => document.getElementById("mirNavFlyoutList");
|
||||
const flyoutTitleEl = () => document.getElementById("mirNavFlyoutTitle");
|
||||
const backBtnEl = () => document.getElementById("mirNavBackBtn");
|
||||
|
||||
function t(key) {
|
||||
return window.I18n?.t(`nav.${key}`) ?? key;
|
||||
}
|
||||
|
||||
function canAccessPage(page) {
|
||||
if (window.AuthApp?.canAccessPage) return window.AuthApp.canAccessPage(page);
|
||||
return true;
|
||||
}
|
||||
|
||||
function visibleItems(moduleId) {
|
||||
const mod = MODULES[moduleId];
|
||||
if (!mod) return [];
|
||||
return mod.items.filter((item) => canAccessPage(item.page));
|
||||
}
|
||||
|
||||
function moduleHasAccess(moduleId) {
|
||||
return visibleItems(moduleId).length > 0;
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_MODULE, activeModule);
|
||||
localStorage.setItem(STORAGE_SECTION, activeSection);
|
||||
localStorage.setItem(STORAGE_FLYOUT, flyoutOpen ? "1" : "0");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function renderFlyout() {
|
||||
const list = flyoutListEl();
|
||||
const title = flyoutTitleEl();
|
||||
if (!list || !title) return;
|
||||
|
||||
const items = visibleItems(activeModule);
|
||||
title.textContent = t(activeModule);
|
||||
list.replaceChildren();
|
||||
|
||||
items.forEach((item) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "mirNavFlyoutItem";
|
||||
btn.dataset.section = item.section;
|
||||
btn.dataset.page = item.page;
|
||||
btn.textContent = t(item.section);
|
||||
if (item.section === activeSection) {
|
||||
btn.classList.add("is-active");
|
||||
btn.setAttribute("aria-current", "page");
|
||||
}
|
||||
btn.addEventListener("click", () => selectSection(item.section, item.page));
|
||||
list.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
function updateRailUI() {
|
||||
document.querySelectorAll(".mirNavRailItem[data-module]").forEach((btn) => {
|
||||
const mod = btn.dataset.module || "";
|
||||
const allowed = moduleHasAccess(mod);
|
||||
btn.hidden = !allowed;
|
||||
btn.style.display = allowed ? "" : "none";
|
||||
const on = mod === activeModule && flyoutOpen;
|
||||
btn.classList.toggle("is-active", on);
|
||||
if (on) btn.setAttribute("aria-current", "true");
|
||||
else btn.removeAttribute("aria-current");
|
||||
});
|
||||
|
||||
const shell = shellEl();
|
||||
if (shell) shell.classList.toggle("mirNavShell--flyout-collapsed", !flyoutOpen);
|
||||
|
||||
const back = backBtnEl();
|
||||
if (back) {
|
||||
const label = flyoutOpen ? t("collapse") : t("expand");
|
||||
back.title = label;
|
||||
back.setAttribute("aria-label", label);
|
||||
}
|
||||
|
||||
renderFlyout();
|
||||
}
|
||||
|
||||
function selectModule(moduleId, opts = {}) {
|
||||
if (!MODULES[moduleId] || !moduleHasAccess(moduleId)) return;
|
||||
|
||||
if (moduleId === activeModule && flyoutOpen && !opts.forceSection) {
|
||||
flyoutOpen = false;
|
||||
saveState();
|
||||
updateRailUI();
|
||||
return;
|
||||
}
|
||||
|
||||
activeModule = moduleId;
|
||||
flyoutOpen = true;
|
||||
|
||||
const items = visibleItems(moduleId);
|
||||
const keepSection = items.some((i) => i.section === activeSection);
|
||||
if (!keepSection || opts.forceSection) {
|
||||
const preferred = items.find((i) => i.section === opts.section) || items[0];
|
||||
if (preferred) {
|
||||
activeSection = preferred.section;
|
||||
if (!opts.skipPage) navigateToPage(preferred.page);
|
||||
}
|
||||
} else if (!opts.skipPage) {
|
||||
const current = items.find((i) => i.section === activeSection);
|
||||
if (current) navigateToPage(current.page);
|
||||
}
|
||||
|
||||
saveState();
|
||||
updateRailUI();
|
||||
}
|
||||
|
||||
function selectSection(section, page) {
|
||||
activeSection = section;
|
||||
saveState();
|
||||
updateRailUI();
|
||||
navigateToPage(page);
|
||||
}
|
||||
|
||||
function navigateToPage(page) {
|
||||
if (window.LmApp?.setActivePage) window.LmApp.setActivePage(page);
|
||||
}
|
||||
|
||||
function syncFromPage(page) {
|
||||
const nav = PAGE_NAV[page];
|
||||
if (!nav) return;
|
||||
activeModule = nav.module;
|
||||
activeSection = nav.section;
|
||||
saveState();
|
||||
updateRailUI();
|
||||
}
|
||||
|
||||
function toggleFlyout() {
|
||||
flyoutOpen = !flyoutOpen;
|
||||
saveState();
|
||||
updateRailUI();
|
||||
}
|
||||
|
||||
function applyPermissions() {
|
||||
const modules = Object.keys(MODULES);
|
||||
if (!moduleHasAccess(activeModule)) {
|
||||
const fallback = modules.find((m) => moduleHasAccess(m));
|
||||
if (fallback) selectModule(fallback, { forceSection: true, skipPage: false });
|
||||
} else {
|
||||
const items = visibleItems(activeModule);
|
||||
if (!items.some((i) => i.section === activeSection)) {
|
||||
activeSection = items[0]?.section || activeSection;
|
||||
}
|
||||
}
|
||||
updateRailUI();
|
||||
}
|
||||
|
||||
function restoreInitialPage() {
|
||||
let page = "config";
|
||||
try {
|
||||
const saved = localStorage.getItem("activePage");
|
||||
if (saved && PAGE_NAV[saved]) page = saved;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
try {
|
||||
const savedMod = localStorage.getItem(STORAGE_MODULE);
|
||||
const savedSec = localStorage.getItem(STORAGE_SECTION);
|
||||
const savedFlyout = localStorage.getItem(STORAGE_FLYOUT);
|
||||
if (savedMod && MODULES[savedMod]) activeModule = savedMod;
|
||||
if (savedSec) activeSection = savedSec;
|
||||
if (savedFlyout === "0") flyoutOpen = false;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
const nav = PAGE_NAV[page];
|
||||
if (nav && moduleHasAccess(nav.module)) {
|
||||
activeModule = nav.module;
|
||||
activeSection = nav.section;
|
||||
} else {
|
||||
const modItems = visibleItems(activeModule);
|
||||
const match = modItems.find((i) => i.page === page) || modItems[0];
|
||||
if (match) {
|
||||
activeSection = match.section;
|
||||
page = match.page;
|
||||
}
|
||||
}
|
||||
|
||||
updateRailUI();
|
||||
navigateToPage(page);
|
||||
}
|
||||
|
||||
function refreshLabels() {
|
||||
window.I18n?.applyDOM?.();
|
||||
updateRailUI();
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
document.querySelectorAll(".mirNavRailItem[data-module]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => selectModule(btn.dataset.module || "setup"));
|
||||
});
|
||||
|
||||
backBtnEl()?.addEventListener("click", toggleFlyout);
|
||||
|
||||
document.getElementById("mirNavLogout")?.addEventListener("click", () => {
|
||||
window.AuthApp?.logout?.();
|
||||
});
|
||||
|
||||
window.addEventListener("lm:locale-change", () => refreshLabels());
|
||||
}
|
||||
|
||||
function init() {
|
||||
refreshLabels();
|
||||
bindEvents();
|
||||
applyPermissions();
|
||||
restoreInitialPage();
|
||||
}
|
||||
|
||||
window.NavApp = {
|
||||
init,
|
||||
syncFromPage,
|
||||
applyPermissions,
|
||||
selectModule,
|
||||
selectSection,
|
||||
toggleFlyout,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user