import m from "mithril" /** Ein Breadcrumb-Glied: Route, Filter-Params und Anzeige-Label einer * Vorgänger-Ansicht in der Aufrufkette. */ export interface iBreadcrumb { route: string params: any label: string } /** Attribute der BreadcrumbBar: die Vorgänger-Kette (ancestors) und das * Label der aktuellen Ansicht (wird als letztes, nicht klickbares Glied * angezeigt). Ist breadcrumbs leer, wird keine Leiste gerendert. * onnavigate (optional): Navigations-Callback, der statt m.route.set * verwendet wird — typischerweise guardedNavigate aus dirtyguard.ts, * so dass bei ungespeicherten Änderungen das Bestätigungs-Modal erscheint. */ export interface iBreadcrumbBar { breadcrumbs: iBreadcrumb[] current: string onnavigate?: (route: string, params: any, e?: Event) => void } /** Parst den Route-Param `bc` (JSON-String) in ein Breadcrumb-Array. * Liefert bei fehlendem/ungültigem `bc` ein leeres Array. */ export function parseBc(bc: string | undefined): iBreadcrumb[] { if (!bc) return [] try { return JSON.parse(bc) } catch { return [] } } /** Serialisiert ein Breadcrumb-Array in den Route-Param `bc`. * Liefert undefined bei leerer Kette (kein bc in der Route). */ export function serializeBc(crumbs: iBreadcrumb[]): string | undefined { return crumbs.length > 0 ? JSON.stringify(crumbs) : undefined } export const BreadcrumbBar: m.ClosureComponent = () => { return { view: (vnode) => { const crumbs = vnode.attrs.breadcrumbs || [] const current = vnode.attrs.current // Ohne Vorgänger und ohne aktuelles Label keine Leiste rendern // (Top-Level-Ansichten ohne Hierarchie-Kontext). if (crumbs.length === 0 && !current) return null const items: any[] = [] crumbs.forEach((crumb: iBreadcrumb, i: number) => { if (i > 0) { items.push(m("span.w3-small", { style: {margin: '0 2px', opacity: '0.6'} }, "›")) } // Vorgänger sind klickbar: Navigation zur jeweiligen Ansicht // mit deren Filter-Params und der auf dieses Glied gekürzten // Kette als neuem bc. items.push(m("a.w3-button.w3-small.w3-theme-l2", { style: { padding: '2px 8px', 'border-radius': '4px', 'text-decoration': 'none', 'white-space': 'nowrap' }, title: "Zurück zu " + crumb.label, onclick: (e: MouseEvent) => { const subChain = crumbs.slice(0, i) const params: any = {...crumb.params} const bc = serializeBc(subChain) if (bc) params.bc = bc // Navigation über den Guard, so dass bei dirty-Maske // das Bestätigungs-Modal erscheint. Event wird // durchgereicht, damit guardedNavigate den auto-redraw // unterbinden kann. if (vnode.attrs.onnavigate) { vnode.attrs.onnavigate(crumb.route, params, e) } else { m.route.set(crumb.route, params) } } }, crumb.label)) }) // Aktuelle Ansicht als letztes, nicht klickbares Glied (fett). if (current) { if (crumbs.length > 0) { items.push(m("span.w3-small", { style: {margin: '0 2px', opacity: '0.6'} }, "›")) } items.push(m("span.w3-small.w3-theme-d3", { style: { padding: '2px 8px', 'border-radius': '4px', 'font-weight': 'bold', 'white-space': 'nowrap' } }, current)) } return m("div.w3-theme-l1", { style: { display: 'flex', 'flex-wrap': 'nowrap', 'align-items': 'center', padding: '4px 8px', 'border-radius': '4px', 'margin-bottom': '8px', 'overflow-x': 'auto', 'overflow-y': 'hidden' } }, items) } } }