Files
kbcrm/Client/Components/widgets/Treetable_V2.ts
T

231 lines
11 KiB
TypeScript

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
}
export const Treetable: m.ClosureComponent<iTreetable> = (vnode) => {
let onRowMarked = vnode.attrs.onRowMarked === undefined ? (node: iTreenode) => {} : vnode.attrs.onRowMarked
let expandtoggle: boolean = true
let searchvalue:string = ""
function setSearchvalue(v:string) {
searchvalue = v
}
return {
view: (vnode) => {
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,
isEven: ((z % 2) == 0)} as iTreerow)
}
else {
return null
}
})
)
)
)
)
}
}
}
interface iTreerow {
treenode: iTreenode
level: number
columnmapper1: Columnmapper
onRowMarked: (node: iTreenode) => void
isEven?: boolean
}
let Treerow: m.ClosureComponent<iTreerow> = (vnode) => {
return {
view: (vnode) => {
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"),
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
? null // Wenn der kinderlos Treenode allerdings sirekt auf Level 0 ist, soll nocht
// eingerückt werden, da es bei Level 0 keine Eltern-Nodes mit Kindern und
// demzufolge mit "expand_more/expand_less"-Button gibt.
: 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,
isEven: ((z % 2) == 0)
} as iTreerow)
})
: null
]
}
}
}
export interface iTopbarButton {
onclick: Function
tooltip?: string
}
export const TopbarButton: m.ClosureComponent<iTopbarButton> = () => {
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<iSearchfield> = (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")
)
)
}
}
}