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 = (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 = { 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) } }