Files
kbcrm/Client/Components/widgets/breadcrumb.ts
T
gg fd30049a32 feat(nav): Breadcrumb-Navigation für Hierarchie-Aufrufkette
Neue BreadcrumbBar-Widget-Komponente (breadcrumb.ts) zeigt die
Aufrufkette als anklickbare Leiste. Der Route-Param bc (JSON-String)
transportiert die Vorgänger-Ansichten durch alle Listen und Masken.
Jede Ansicht hängt beim Weiternavigieren ihren eigenen Crumb an die
Kette an; Masken reichen bc beim Speichern/Abbrechen zurück.
Erfasst: kunde, projekt, kontaktadresse, call_log, kontakt.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-19 16:16:49 +02:00

100 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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. */
export interface iBreadcrumbBar {
breadcrumbs: iBreadcrumb[]
current: string
}
/** 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<iBreadcrumbBar> = () => {
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: () => {
const subChain = crumbs.slice(0, i)
const params: any = {...crumb.params}
const bc = serializeBc(subChain)
if (bc) params.bc = bc
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)
}
}
}