Files
gg 929664453e feat(form): flexContent-Option für Flex-Spalte im Formular-Inhalt
Der scrollbare Inhaltsbereich der Form kann bei Bedarf als Flex-Spalte
gerendert werden, damit Masken-Kinder den verbleibenden Platz füllen und
in sich scrollen können, statt die gesamte Maske scrollbar zu machen.
Wirkt nur zusammen mit stickyButtons (Default true).

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

205 lines
9.6 KiB
TypeScript

import m from "mithril"
export interface iForm {
onchanged?: (e:Event) => void
onsubmit: (form:HTMLFormElement) => boolean
oncancel: (e:Event) => void
showbuttons?: boolean
/** Wenn true (Default), bleiben die Buttons am unteren Rand fixiert,
* während der Formularinhalt scrollt. Bei false scrollen die Buttons
* mit dem Formular. */
stickyButtons?: boolean
/** Die Funktion gibt das HTMLFormElement zurück, welches in der Form-Componente angelegt wird */
onformcreate?: (form:HTMLFormElement) => void
/** Callback bei Änderung des Dirty-State. Wird mit true aufgerufen,
* sobald der Nutzer Eingaben vornimmt, und mit false, nachdem das
* Formular erfolgreich gespeichert oder zurückgesetzt wurde. Die
* umgebende Maske nutzt dies, um ihren eigenen isDirty-State mit dem
* der Form synchron zu halten und so den Navigations-Guard
* (dirtyguard.ts) zu speisen. */
onDirtyChange?: (dirty: boolean) => void
/** Zusätzliche Buttons, die in die Standard-Button-Leiste der Form
* eingereiht werden. Sie werden im DOM vor den Standard-Buttons
* gesetzt und floaten daher (mit w3-right) rechts außen — die
* Standard-Buttons (Zurücksetzen, Abbrechen, Speichern) behalten
* ihre gewohnte Position. Die Buttons sind selbst als m(...) zu
* übergeben; die aufrufende Maske ist für Styling und Callbacks
* verantwortlich. */
extrabuttons?: m.Child[]
/** Wenn true, wird der scrollbare Inhaltsbereich der Form als
* Flex-Spalte gerendert. Masken können dann einzelne Kinder mit
* flex-Styles ausstatten, z.B. einen Bereich, der den verbleibenden
* Platz füllt (flex: '01 1 auto' + 'min-height': '0') und in sich
* scrollt, statt die gesamte Maske scrollbar zu machen. Wirkt nur
* zusammen mit stickyButtons (Default true). */
flexContent?: boolean
}
export const Form:m.ClosureComponent<iForm> = (vnode) => {
let form: HTMLFormElement
let isDirty: boolean = false
let showButtons: boolean = vnode.attrs.showbuttons == undefined ? true : vnode.attrs.showbuttons
// Default: Buttons bleiben fixiert am unteren Rand (stickyButtons = true).
let stickyButtons: boolean = vnode.attrs.stickyButtons == undefined ? true : vnode.attrs.stickyButtons
function canCloseForm(typ: string = "") {
if (!isDirty) {
return true
}
let text = (typ == "reset") ? "zurücksetzen" : "beenden"
return confirm (`Das Formular enthält noch ungespeicherte Eingaben. Wollen Sie das Formular trotzdem ${text}?`)
}
function onchanged (e:Event) {
isDirty = true
if (vnode.attrs.onDirtyChange != undefined) {
vnode.attrs.onDirtyChange(true)
}
if (vnode.attrs.onchanged != undefined) {
vnode.attrs.onchanged(e)
}
}
function submit (e:Event) {
e.preventDefault()
if (vnode.attrs.onsubmit(form) == true) {
isDirty = false
if (vnode.attrs.onDirtyChange != undefined) {
vnode.attrs.onDirtyChange(false)
}
}
}
function registerFormElement (f:HTMLFormElement) {
if (vnode.attrs.onformcreate != undefined) {
vnode.attrs.onformcreate(f)
}
}
function cancel(e:Event) {
e.preventDefault()
// Der Dirty-Check beim Abbrechen erfolgt jetzt durch den
// Navigations-Guard der Maske (dirtyguard.ts), da oncancel die
// Navigation auslöst. Die Form selbst fragt hier nicht mehr nach,
// um eine doppelte Bestätigung zu vermeiden. isDirty wird nicht
// zurückgesetzt, damit der Guard den aktuellen State sieht.
if (vnode.attrs.oncancel != undefined) {
vnode.attrs.oncancel(e)
}
else {
history.back()
}
}
function reset(e:Event) {
if (canCloseForm("reset")) {
isDirty = false
if (vnode.attrs.onDirtyChange != undefined) {
vnode.attrs.onDirtyChange(false)
}
// Hier passiert nichts.
// Da mithril nach jedem User-Event (wie z.B. einem 'Click') ein redraw() ausführt, wird hier die Form
// neu gezeichnet und zwar mit den ursprünglichen Daten aus den zu grundeliegenden Datenmodel, das ja
// ohne submit noch nicht geändert wurde.
return
}
e['redraw'] = false
return
}
return {
onremove: (vnode) => {
let message = "remove"
if (isDirty) {
message += " isDirty"
}
console.log(message)
},
view: (vnode) => {
const buttons = showButtons == true
? m("div.w3-bar.w3-padding-16",
{
style: stickyButtons
? {flex: '0 0 auto', position: 'sticky', bottom: '0', 'background-color': 'white'}
: {}
},
[
// Zusätzliche Buttons der Maske zuerst ins DOM:
// mit w3-right landen sie optisch rechts außen,
// die Standard-Buttons bleiben links davon in
// ihrer gewohnten Reihenfolge.
...(vnode.attrs.extrabuttons || []),
m("button.w3-button.w3-theme.w3-right.w3-mobile", {style: {margin: '0px 0px 2px 4px'}, type: "submit"}, "Speichern"),
m("button.w3-button.w3-theme.w3-right.w3-mobile", {style: {margin: '0px 0px 2px 4px'}, onclick: cancel, type: "button"}, "Abbrechen"),
m("button.w3-button.w3-theme.w3-right.w3-mobile", {onclick: reset, type: "button"}, "Zurücksetzen")
])
: null
// Bei stickyButtons liegt der Formularinhalt in einem scrollbaren
// Container, die Buttons bleiben am unteren Rand fixiert. Das
// umschließende form-Element wird zum flex-Container, damit das
// Layout auch bei Fenstergrößenänderung stabil bleibt.
// Stil des scrollbaren Inhaltsbereichs; optional als Flex-Spalte
// (flexContent), damit Masken-Kinder den verbleibenden Platz
// füllen können.
const contentStyle: any = {flex: '1', 'overflow-y': 'auto', 'padding-right': '8px'}
if (vnode.attrs.flexContent === true) {
contentStyle.display = 'flex'
contentStyle['flex-direction'] = 'column'
}
if (stickyButtons) {
return m("form", {
style: {display: 'flex', 'flex-direction': 'column', height: '100%'},
onreset: () => {console.log("reset")},
onsubmit: submit,
oncreate: (vnode) => {
form = vnode.dom as HTMLFormElement
let elements = form.querySelectorAll("input,textarea")
for (let index = 0; index < elements.length; index++) {
const element = elements[index];
element.addEventListener("change", onchanged)
// 'input' feuert schon während des Tippens (vor
// Blur), so dass die Maske dirty wird, auch wenn
// der Nutzer sofort eine Breadcrumb klickt.
element.addEventListener("input", onchanged)
}
registerFormElement(form)
}
},
// Scrollbarer Formular-Inhalt.
m("div", {style: contentStyle},
vnode.children
),
// Fixierter Button-Bereich.
buttons
)
}
// Ohne stickyButtons: bisheriges Verhalten (Buttons scrollen mit).
return m("form", {
onreset: () => {console.log("reset")},
onsubmit: submit,
oncreate: (vnode) => {
form = vnode.dom as HTMLFormElement
let elements = form.querySelectorAll("input,textarea")
for (let index = 0; index < elements.length; index++) {
const element = elements[index];
element.addEventListener("change", onchanged)
// 'input' feuert schon während des Tippens (vor
// Blur), so dass die Maske dirty wird, auch wenn
// der Nutzer sofort eine Breadcrumb klickt.
element.addEventListener("input", onchanged)
}
registerFormElement(form)
}
},
vnode.children,
buttons
)
}
}
}
export interface iFormBorder {
maxwidth: string
}
export const FormBorder: m.Component<iFormBorder> = {
view: (vnode) => {
let mw:string = vnode.attrs.maxwidth === undefined ? "600px" : vnode.attrs.maxwidth
return m("div.w3-border.w3-border-theme.w3-container.w3-content.w3-margin-top",
{style: {'max-width': mw, 'box-shadow': '3px 3px 3px darkgrey'}}, vnode.children)
}
}