feat(treetable): optionale Kopfzeile und abschaltbare Expand-Buttons

Neues Attribut header rendert eine sticky Spaltenüberschriftenzeile.
showExpandButtons=false blendet die Ein-/Ausklappen-Buttons aus; die
Topbar entfällt dann ganz, wenn auch keine Children oder Filter vorhanden
sind — der scrollbare Bereich nutzt in dem Fall die volle Höhe.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-24 18:54:42 +02:00
1 parent 3a6732e36b
commit 981b28df03
1 file changed
+68 -17
+68 -17
View File
@@ -39,6 +39,20 @@ export interface iTreetable {
* <tr>-Zeile; die columnmapper-Inhalte brauchen dann kein eigenes
* Selektions-Styling mehr. */
isSelected?: (node: iTreenode) => boolean
/** Optional: Spaltenüberschriften (ein Titel je columnmapper-Spalte,
* 1:1 von links nach rechts). Wenn gesetzt, rendert das Widget eine
* Kopfzeile über den Datenzeilen. Hinweis zur ersten Spalte: Die
* Einrückung/der Expand-Pfeil der Baumdarstellung liegt im selben
* td wie die ERSTE columnmapper-Zelle — der erste Titel steht
* daher direkt über der ersten Datenspalte. Die Kopfzeile bleibt
* beim Scrollen sticky sichtbar. */
header?: string[]
/** Optional: Ein-/Ausklappen-Buttons (unfold_more/unfold_less) in der
* Topbar anzeigen. Default true. Für flache Listen ohne Hierarchie
* auf false setzen — die Topbar entfällt dann ganz, sofern auch
* keine Children (z.B. TopbarButtons) übergeben und kein filter
* gesetzt ist. */
showExpandButtons?: boolean
}
export const Treetable: m.ClosureComponent<iTreetable> = (vnode) => {
@@ -58,26 +72,63 @@ export const Treetable: m.ClosureComponent<iTreetable> = (vnode) => {
const topHasSiblingsWithChildren = vnode.attrs.treenodes.some(
(tn: iTreenode) => tn.hasChildren
)
// Topbar (Expand-Buttons, Children, Suchfeld) nur rendern, wenn
// tatsächlich Inhalt vorhanden ist — bei flachen Listen ohne
// Expand-Buttons, Children und filter bleibt sie komplett weg.
const showExpand = vnode.attrs.showExpandButtons !== false
// vnode.children kann ein Array oder ein Primitiv sein
// (Mithril-Typ ChildArrayOrPrimitive).
const hatChildren = Array.isArray(vnode.children)
? vnode.children.length > 0
: vnode.children != null
const hatTopbar = showExpand || hatChildren || vnode.attrs.filter != undefined
return m("div.w3-border-theme", {style: {height: '99%', overflow: 'hidden', 'border-style': 'solid', 'border-width': '1px'}},
m("div.w3-bar.w3-theme",
m(TopbarButton, {
onclick: () => {
expandtoggle = true; changeTreenodes(vnode.attrs.treenodes, expandtoggle)
}},
m("i.material-icons", "unfold_more")
),
m(TopbarButton, {
onclick: () => {
expandtoggle = false; changeTreenodes(vnode.attrs.treenodes, expandtoggle)
}},
m("i.material-icons", "unfold_less")
),
vnode.children,
(vnode.attrs.filter != undefined) ? m(SearchField, {setSearchvalue: setSearchvalue} as iSearchfield) : null
),
hatTopbar
? m("div.w3-bar.w3-theme",
showExpand
? [
m(TopbarButton, {
onclick: () => {
expandtoggle = true; changeTreenodes(vnode.attrs.treenodes, expandtoggle)
}},
m("i.material-icons", "unfold_more")
),
m(TopbarButton, {
onclick: () => {
expandtoggle = false; changeTreenodes(vnode.attrs.treenodes, expandtoggle)
}},
m("i.material-icons", "unfold_less")
)
]
: null,
vnode.children,
(vnode.attrs.filter != undefined) ? m(SearchField, {setSearchvalue: setSearchvalue} as iSearchfield) : null
)
: null,
m("div", {style: {height: '100%'}},
m("div", {style: {height: 'calc(100% - 32px', overflow: 'auto', width: '100%'}},
// Ohne Topbar nutzt der scrollbare Bereich die volle Höhe.
m("div", {style: {height: hatTopbar ? 'calc(100% - 32px' : '100%', overflow: 'auto', width: '100%'}},
m("table.w3-table", {style: {'overflow-y': 'auto'}},
// Optionale Spaltenüberschriften: Ein Titel je
// columnmapper-Spalte, 1:1 von links nach rechts
// (die Einrückung teilt sich das td mit der
// ersten Datenspalte, keine Führungs-Spalte).
// Die Kopfzeile bleibt beim Scrollen sticky.
vnode.attrs.header
? m("thead",
m("tr.w3-theme",
vnode.attrs.header.map((titel: string) =>
m("th", {
style: {
position: 'sticky',
top: '0',
'z-index': '1',
'text-align': 'left',
'white-space': 'nowrap'
},
title: titel
}, titel))))
: null,
vnode.attrs.treenodes.map((tn:iTreenode, z) => {
if (vnode.attrs.filter == undefined || (vnode.attrs.filter != undefined && vnode.attrs.filter(tn, searchvalue) == true)) {
return m(Treerow, {