WebUI-Kopfleiste: Aufklappmenues statt elf Tabs
Umsetzung von #541, folgt auf #532. Elf Tabs nebeneinander waren nicht mehr zu ueberblicken. Die Leiste hat jetzt vier Eintraege, gruppiert nach dem, was man dort tut: - Dashboard - Analytics: Analytics, Accounting, Supervisor - Module: Moduleinstellungen (die allgemeine Modulansicht), danach alle Module - Einstellungen: Einstellungen, Jobs, Konten, Terminal / Logs Supervisor und Accounting stehen unter Analytics und zusaetzlich in der Modulliste, weil dort alle Module stehen sollen. Hervorgehoben wird das Oberelement nur an ihrem Hauptplatz (Analytics); der zweite Zugang traegt data-secondary. Sonst leuchteten bei geoeffnetem Supervisor zwei Menues. switchView() findet den aktiven Eintrag jetzt ueber data-view statt ueber eine Teilzeichenkette im onclick-Attribut. Das alte Verfahren hing am Zufall der Namen: Eine Ansicht, deren Name in einer anderen enthalten ist, haette beide markiert. Die Modulliste zeigt den Zustand aus /api/modules: Ein deaktiviertes Modul erscheint ausgegraut mit "deaktiviert" und ist nicht anklickbar, statt in eine leere Ansicht zu fuehren. Ein Modul, das der Server fuehrt, fuer das es im WebUI aber keine Ansicht gibt, wird mit "keine Ansicht" gelistet. Bedienbar mit Maus und Tastatur: Klick ausserhalb und Esc schliessen (Esc gibt den Fokus ans Oberelement zurueck), Pfeil nach unten oeffnet und springt in das Menue, Pfeiltasten laufen um und ueberspringen deaktivierte Eintraege, Tab verlaesst das Menue. aria-haspopup, aria-expanded und aria-current sind gesetzt. Unter 760 px haengen die Menues an der ganzen Leiste statt am Eintrag. Bei 400 px ragte "Module" sonst 35 px ueber den rechten Rand. Ein Nachmessen per Skript taugt dagegen nicht: In der Mobilansicht waechst der Layout-Viewport mit dem Ueberstand mit, und die Messung meldet genau dann "passt". Die Modulansicht heisst jetzt wie ihr Menueeintrag "Moduleinstellungen". Geprueft auf der Linux-Test-VM im Browser: Alle 12 Eintraege schalten die richtige Ansicht und heben das richtige Oberelement hervor, das Menue schliesst danach; Tastaturbedienung, deaktiviertes und unbekanntes Modul (simuliert) verhalten sich wie beschrieben. Vermessen bei 400, 700, 768 und 1024 px: kein Menue ragt aus dem Fenster, die Seite scrollt nie waagerecht. 556 von 556 Tests bestanden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ let allLogs = [];
|
||||
|
||||
// Init on DOM ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initNavMenus();
|
||||
refreshNavModules();
|
||||
initSseLogs();
|
||||
refreshDashboard();
|
||||
loadDashboardAccounts();
|
||||
@@ -18,12 +20,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Navigation
|
||||
function switchView(viewName) {
|
||||
currentView = viewName;
|
||||
document.querySelectorAll('.nav-link').forEach(btn => btn.classList.remove('active'));
|
||||
markActiveNav(viewName);
|
||||
closeNavDropdowns();
|
||||
document.querySelectorAll('.view-panel').forEach(p => p.classList.remove('active'));
|
||||
|
||||
const activeBtn = Array.from(document.querySelectorAll('.nav-link')).find(b => b.getAttribute('onclick')?.includes(viewName));
|
||||
if (activeBtn) activeBtn.classList.add('active');
|
||||
|
||||
const panel = document.getElementById(`view-${viewName}`);
|
||||
if (panel) panel.classList.add('active');
|
||||
|
||||
@@ -49,6 +49,143 @@ function switchView(viewName) {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Kopfleiste: Aufklappmenues (#541) ====================
|
||||
|
||||
/**
|
||||
* Hebt den Eintrag der aktiven Ansicht hervor und dazu das Oberelement seines Menues -
|
||||
* sonst sieht man bei geschlossenem Menue nicht, wo man ist. Supervisor und Accounting
|
||||
* stehen zweimal im Menue; das Oberelement leuchtet nur an ihrem Hauptplatz (Analytics),
|
||||
* der zweite Zugang in der Modulliste traegt data-secondary.
|
||||
*/
|
||||
function markActiveNav(viewName) {
|
||||
document.querySelectorAll('.header-nav [data-view]').forEach(el => {
|
||||
const on = el.dataset.view === viewName;
|
||||
el.classList.toggle('active', on);
|
||||
if (on) el.setAttribute('aria-current', 'page');
|
||||
else el.removeAttribute('aria-current');
|
||||
});
|
||||
|
||||
document.querySelectorAll('.nav-dropdown').forEach(dd => {
|
||||
const home = dd.querySelector(`[data-view="${viewName}"]:not([data-secondary])`);
|
||||
dd.querySelector('.nav-dropdown-toggle').classList.toggle('active', !!home);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleNavDropdown(toggle) {
|
||||
const menu = toggle.closest('.nav-dropdown').querySelector('.nav-menu');
|
||||
const willOpen = menu.hidden;
|
||||
closeNavDropdowns();
|
||||
if (willOpen) {
|
||||
menu.hidden = false;
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
function closeNavDropdowns() {
|
||||
document.querySelectorAll('.nav-dropdown').forEach(dd => {
|
||||
dd.querySelector('.nav-menu').hidden = true;
|
||||
dd.querySelector('.nav-dropdown-toggle').setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function navMenuItems(menu) {
|
||||
return Array.from(menu.querySelectorAll('.nav-menu-item')).filter(b => !b.disabled);
|
||||
}
|
||||
|
||||
function initNavMenus() {
|
||||
// Klick ausserhalb schliesst. Der Klick auf ein Oberelement selbst laeuft ueber
|
||||
// toggleNavDropdown und kommt hier mit closest('.nav-dropdown') an - bleibt also offen.
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.closest('.nav-dropdown')) closeNavDropdowns();
|
||||
});
|
||||
|
||||
document.querySelector('.header-nav')?.addEventListener('keydown', e => {
|
||||
const dd = e.target.closest('.nav-dropdown');
|
||||
if (!dd) return;
|
||||
const toggle = dd.querySelector('.nav-dropdown-toggle');
|
||||
const menu = dd.querySelector('.nav-menu');
|
||||
|
||||
if (e.key === 'Escape' && !menu.hidden) {
|
||||
e.preventDefault();
|
||||
closeNavDropdowns();
|
||||
toggle.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Pfeil nach unten auf dem Oberelement oeffnet und springt in das Menue.
|
||||
if (e.target === toggle && e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (menu.hidden) toggleNavDropdown(toggle);
|
||||
navMenuItems(menu)[0]?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (menu.contains(e.target) && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
const items = navMenuItems(menu);
|
||||
const i = items.indexOf(e.target);
|
||||
const next = e.key === 'Home' ? 0
|
||||
: e.key === 'End' ? items.length - 1
|
||||
: e.key === 'ArrowDown' ? (i + 1) % items.length
|
||||
: (i - 1 + items.length) % items.length;
|
||||
items[next]?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Tab verlaesst das Menue, statt durch alle Eintraege zu wandern.
|
||||
if (e.key === 'Tab' && !menu.hidden) closeNavDropdowns();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt deaktivierte Module im Menue als deaktiviert und ergaenzt die Modulliste um
|
||||
* Module, die der Server fuehrt, fuer die es im WebUI aber noch keine Ansicht gibt.
|
||||
* Ohne Uebergabe wird die Liste selbst geholt; loadModules() reicht ihre mit durch.
|
||||
*/
|
||||
async function refreshNavModules(modules) {
|
||||
if (!modules) {
|
||||
try {
|
||||
const res = await fetch('/api/modules');
|
||||
if (!res.ok) return;
|
||||
modules = await res.json();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const isActive = new Map(modules.map(m => [m.name.toLowerCase(), m.isActive]));
|
||||
|
||||
document.querySelectorAll('.header-nav [data-module]').forEach(item => {
|
||||
const off = isActive.get(item.dataset.module.toLowerCase()) === false;
|
||||
item.disabled = off;
|
||||
item.title = off ? 'Modul ist deaktiviert – siehe Moduleinstellungen' : '';
|
||||
const hint = item.querySelector('.nav-menu-hint');
|
||||
if (hint) hint.textContent = off ? 'deaktiviert' : '';
|
||||
});
|
||||
|
||||
const list = document.getElementById('navModuleList');
|
||||
if (!list) return;
|
||||
list.querySelectorAll('[data-no-view]').forEach(e => e.remove());
|
||||
|
||||
const shown = new Set(Array.from(list.querySelectorAll('[data-module]')).map(e => e.dataset.module.toLowerCase()));
|
||||
modules.filter(m => !shown.has(m.name.toLowerCase())).forEach(m => {
|
||||
const item = document.createElement('button');
|
||||
item.className = 'nav-menu-item';
|
||||
item.setAttribute('role', 'menuitem');
|
||||
item.disabled = true;
|
||||
item.dataset.noView = '1';
|
||||
item.title = 'Für dieses Modul gibt es im WebUI noch keine eigene Ansicht.';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = m.name;
|
||||
const hint = document.createElement('span');
|
||||
hint.className = 'nav-menu-hint';
|
||||
hint.textContent = m.isActive ? 'keine Ansicht' : 'deaktiviert';
|
||||
item.append(label, hint);
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Dashboard Refresh
|
||||
async function refreshDashboard() {
|
||||
try {
|
||||
@@ -337,6 +474,7 @@ async function loadModules() {
|
||||
const res = await fetch('/api/modules');
|
||||
if (!res.ok) return;
|
||||
const modules = await res.json();
|
||||
refreshNavModules(modules);
|
||||
|
||||
const container = document.getElementById('modulesContainer');
|
||||
container.innerHTML = modules.map(m => {
|
||||
|
||||
Reference in New Issue
Block a user