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>
100 lines
3.8 KiB
TypeScript
100 lines
3.8 KiB
TypeScript
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)
|
||
}
|
||
}
|
||
}
|