Files
App/www/nav.js
HiepLM a6cf06d7eb
Some checks failed
Test / test (push) Has been cancelled
Add phần create map by upload
2026-06-19 11:52:21 +07:00

301 lines
8.4 KiB
JavaScript

/**
* 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-list", page: "dashboard" }],
dynamic: true,
},
setup: {
items: [
{ section: "missions", page: "missions" },
{ section: "maps", page: "maps" },
{ section: "build-robot", 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-list" },
config: { module: "setup", section: "build-robot" },
maps: { 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 = "missions";
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 moduleItems(moduleId) {
const mod = MODULES[moduleId];
if (!mod) return [];
if (mod.dynamic && moduleId === "dashboards" && window.DashboardApp?.getNavItems) {
return window.DashboardApp.getNavItems();
}
return mod.items;
}
function visibleItems(moduleId) {
const mod = MODULES[moduleId];
if (!mod) return [];
return moduleItems(moduleId).filter((item) => canAccessPage(item.page));
}
function itemLabel(item) {
if (item.label) return item.label;
return t(item.section);
}
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 = itemLabel(item);
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);
if (page === "dashboard") window.DashboardApp?.handleNav?.(section);
}
function syncDashboardSection(section) {
activeModule = "dashboards";
activeSection = section.startsWith("dashboard-") || section === "dashboard-list" ? section : `dashboard-${section}`;
saveState();
updateRailUI();
}
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();
if (page === "dashboard") window.DashboardApp?.handleNav?.(activeSection);
}
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 = "missions";
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 refreshFlyout() {
updateRailUI();
}
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,
syncDashboardSection,
applyPermissions,
selectModule,
selectSection,
toggleFlyout,
refreshFlyout,
};
})();