import m from "mithril" export interface iTreenode { nodevalue: any children: iTreenode[] expand: boolean hasChildren: boolean isChecked?: boolean } export class Treenode implements iTreenode { nodevalue:any = null children: iTreenode[] = [] expand: boolean = true isChecked?: boolean = false constructor(nodevalue: any = null, expand: boolean = false, children: iTreenode[] = [], isChecked: boolean = false) { this.nodevalue = nodevalue this.children = children this.expand = expand this.isChecked = isChecked } get hasChildren() { return (this.children.length > 0) } } export interface Columnmapper { (row:iTreenode) : any[] } export interface iTreetable { treenodes: iTreenode[] columnmapper: Columnmapper onRowMarked?: (node: iTreenode) => void filter?: (row:iTreenode, searchvalue:string) => boolean /** Optional: Callback, der für eine Treenode bestimmt, ob die gesamte * Tabellenzeile als markiert (selektiert) dargestellt werden soll. * Wenn gesetzt, übernimmt das Widget die Hintergrund-Einfärbung der * -Zeile; die columnmapper-Inhalte brauchen dann kein eigenes * Selektions-Styling mehr. */ isSelected?: (node: iTreenode) => boolean } export const Treetable: m.ClosureComponent = (vnode) => { let onRowMarked = vnode.attrs.onRowMarked === undefined ? (node: iTreenode) => {} : vnode.attrs.onRowMarked let isSelected = vnode.attrs.isSelected let expandtoggle: boolean = true let searchvalue:string = "" function setSearchvalue(v:string) { searchvalue = v } return { view: (vnode) => { // Prüfen, ob mindestens ein Top-Level-Node Kinder hat. // Wird benötigt, um bei kinderlosen Top-Level-Nodes den // Platzhalter-Pfeil nur dann zu rendern, wenn es Geschwister // mit Kindern (und damit Pfeil) gibt. const topHasSiblingsWithChildren = vnode.attrs.treenodes.some( (tn: iTreenode) => tn.hasChildren ) 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 ), m("div", {style: {height: '100%'}}, m("div", {style: {height: 'calc(100% - 32px', overflow: 'auto', width: '100%'}}, m("table.w3-table", {style: {'overflow-y': 'auto'}}, 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, { // ACHTUNG: Änderungen hier müssen unten beim rekursiven Aufruf der Treerow-Funktion ebenfalls eingebaut werden treenode: tn, level: 0, columnmapper1: vnode.attrs.columnmapper, onRowMarked: onRowMarked, isSelected: isSelected, isEven: ((z % 2) == 0), hasSiblingsWithChildren: topHasSiblingsWithChildren} as iTreerow) } else { return null } }) ) ) ) ) } } } interface iTreerow { treenode: iTreenode level: number columnmapper1: Columnmapper onRowMarked: (node: iTreenode) => void isSelected?: (node: iTreenode) => boolean isEven?: boolean /** True, wenn mindestens ein Geschwister-Node auf demselben Level * Kinder hat. Wird verwendet, um bei kinderlosen Top-Level-Nodes * den Platzhalter-Pfeil nur dann zu rendern, wenn es Nodes mit * Pfeil gibt, an denen der Text ausgerichtet werden kann. */ hasSiblingsWithChildren?: boolean } let Treerow: m.ClosureComponent = (vnode) => { return { view: (vnode) => { // Wenn ein isSelected-Callback gesetzt ist, prüfen, ob diese // Zeile selektiert ist; in diesem Fall wird der Hintergrund der // gesamten -Zeile eingefärbt. const rowSelected = vnode.attrs.isSelected ? vnode.attrs.isSelected(vnode.attrs.treenode) : false // Prüfen, ob mindestens ein Child-Node selbst Kinder hat. const childHasSiblingsWithChildren = vnode.attrs.treenode.children.some( (tn: iTreenode) => tn.hasChildren ) return [ m("tr.w3-border-theme", {onmouseup: () => vnode.attrs.onRowMarked(vnode.attrs.treenode), onkeyup: () => vnode.attrs.onRowMarked(vnode.attrs.treenode), class: (vnode.attrs.isEven ? "w3-tablerow-even" : "w3-tablerow-odd") + (rowSelected ? " w3-theme-l4" : ""), style: {'border-top-style': 'solid', 'border-top-width': '1px'} }, m("td", // Zunächst wird ein td-Element für die erste Spalte der Tabelle erzeugt, damit dort die // Einrückung in Abhängigkeit vom evtl. Vorhandensein von Kind-Elementen erledigt wird m("div", {style: {width: 'max-content', 'margin-left': `${(vnode.attrs.level * 25)}px`}} , vnode.attrs.treenode.hasChildren // Wenn der Treenode Kinder hat, dann soll der "expand_more/expand_less"-Button // angezeigt werden ? m("span.w3-button", { onclick: () => {changeTreenode(vnode.attrs.treenode, !vnode.attrs.treenode.expand)}, style: {padding: '0px', 'margin-right': '8px', width: '24px', height: '24px'}}, m("i.material-icons", vnode.attrs.treenode.expand?"expand_more":"navigate_next") ) // Wenn der Trennode keine Kinder hat, dann soll kein "expand_more/expand_less"-Button // angezeigt werden, allerdings soll durch 'margin-left' der entsprechende Platz des // Buttons eingerückt werden, damit die Einrückungs-Level auch bei "kinderlosen" Treenodes // denen der Trennodes mit Kindern entsprechen : vnode.attrs.level == 0 ? (vnode.attrs.hasSiblingsWithChildren ? m("span", { style: {'margin-left': '0px', 'margin-right': '32px'} }) : null) : m( "span", { style: {'margin-left': '24px', 'margin-right': '8px'} } ), // Nachdem die einrückung auf Grund der "hasChildren"-Abfrage erledigt ist, wird der Inhalt // der ersten Spalte des Column-Mappers angezeigt vnode.attrs.columnmapper1(vnode.attrs.treenode).splice(0,1) ) ), // Dann werden die übrigen Spalten ganz normal als td-Elemente eingefügt vnode.attrs.columnmapper1(vnode.attrs.treenode).splice(1).map((column) => { return m("td", column) }) ), // Falls der Trennode Kinder hat und expandiert werden soll, wird rekursiv die Trennode-Komponente für // die Child-Nodes aufgerufen, ansonsten wir null zurückgegeben und die Einrückung hört auf vnode.attrs.treenode.hasChildren && vnode.attrs.treenode.expand ? vnode.attrs.treenode.children.map((tn: iTreenode, z) => { return m(Treerow, { treenode: tn, level: vnode.attrs.level + 1, columnmapper1: vnode.attrs.columnmapper1, onRowMarked: vnode.attrs.onRowMarked, isSelected: vnode.attrs.isSelected, isEven: ((z % 2) == 0), hasSiblingsWithChildren: childHasSiblingsWithChildren } as iTreerow) }) : null ] } } } export interface iTopbarButton { onclick: Function tooltip?: string } export const TopbarButton: m.ClosureComponent = () => { return { view: (vnode) => { return m("button.w3-button.w3-bar-item", { style: {'min-width': '32px', height: '32px', padding: '4px'}, onclick: vnode.attrs.onclick, title: vnode.attrs.tooltip?vnode.attrs.tooltip:null }, vnode.children ) } } } /** Die Funktion kann aufgerufen werden, wenn eine Treenode-Struktur hinsichtlich * "Expand more/less" geändert werden soll. */ export function changeTreenodes(treenodes: Treenode[], expand: boolean) { treenodes.forEach((treenode: Treenode) => { changeTreenode(treenode, expand) if (treenode.hasChildren) { changeTreenodes(treenode.children, expand) } }) } function changeTreenode(treenode: Treenode, expand: boolean) { treenode.expand = expand } interface iSearchfield { setSearchvalue: (v:string) => void } const SearchField:m.ClosureComponent = (vnode) => { let inputfield: HTMLInputElement|null = null function oninput(e:Event) { if (inputfield) { // ✅ Null-Check let v:string = inputfield.value vnode.attrs.setSearchvalue(v) } } return { view: (vnode) => { return m("div", {style: {display: "flex", border: "1px solid black", margin: '1px'}}, m("input.ggg-inputfocus", { oncreate: (vnode) => { inputfield = vnode.dom as HTMLInputElement inputfield.focus() }, type:"text", style: {flex: '1', border: 'none'}, oninput: oninput }), m("button.w3-button", { style: {'min-width': '28px', height: '28px', padding: '4px', border: 'none'}, onclick: () => { if (inputfield) { // ✅ Null-Check inputfield.value = "" vnode.attrs.setSearchvalue("") inputfield.focus() } }}, m("i.material-icons", {style: {'font-size': 'medium'}}, "clear") ) ) } } }