diff --git a/Client/Components/call_log.ts b/Client/Components/call_log.ts index 070ffb2..ee1f9e3 100644 --- a/Client/Components/call_log.ts +++ b/Client/Components/call_log.ts @@ -256,9 +256,13 @@ export const Call_Log: m.ClosureComponent = (vnode) => { return {value: p.id, label: p.projektname} }) // Einheit der Dauer-Anzeige im Format mm:ss. - return m("div.w3-container.w3-auto", + return m("div.w3-container.w3-auto.form-view-container", m("h1", currentLog.id === -1 ? "Neue Gesprächsnotiz anlegen" : "Gesprächsnotiz"), - m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [ + // Form füllt den verbleibenden Platz; das Form-Widget selbst + // sorgt (mit stickyButtons, Default) dafür, dass nur der + // Inhalt scrollt und die Buttons fixiert bleiben. + m("div", {style: {flex: '1', 'min-height': '0'}}, + m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [ m("label", "Kunde", m(Select, { name: "kundenId", @@ -303,7 +307,8 @@ export const Call_Log: m.ClosureComponent = (vnode) => { m("label", "Notiz", m(Textarea, {name: "notiz", value: currentLog.notiz, rows: 4} as iTextarea)), m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlageDatum", value: currentLog.wiedervorlageDatum, type: "date"} as iInput)), m("label", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt") - ]) + ]) + ) ) } } diff --git a/Client/Components/kunde.ts b/Client/Components/kunde.ts index 0186312..238cf1d 100644 --- a/Client/Components/kunde.ts +++ b/Client/Components/kunde.ts @@ -51,15 +51,17 @@ export const kunde: m.ClosureComponent = (vnode) => { const cancel = (e: Event) => { m.route.set("/kundenliste") } - return m("div.w3-container.w3-auto", + return m("div.w3-container.w3-auto.form-view-container", m("h1", currentKunde.id === -1 ? "Neuen Kunden anlegen" : "Kundenstammblatt"), - m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [ + m("div", {style: {flex: '1', 'min-height': '0'}}, + m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [ m("label", "Kundenname", m(Input, {name: "kundenname", value: currentKunde.name, type: "text", required: true} as iInput)), m("label", "Straße", m(Input, {name: "strasse", value: currentKunde.strasse, type: "text"} as iInput)), m("label", "PLZ", m(Input, {name: "plz", value: currentKunde.plz, type: "text"} as iInput)), m("label", "Ort", m(Input, {name: "ort", value: currentKunde.ort, type: "text"} as iInput)), m("label", "Land", m(Input, {name: "land", value: currentKunde.land, type: "text"} as iInput)) - ]) + ]) + ) ) } } diff --git a/Client/Components/projekt.ts b/Client/Components/projekt.ts index 43628d1..ea1c964 100644 --- a/Client/Components/projekt.ts +++ b/Client/Components/projekt.ts @@ -217,9 +217,10 @@ export const Projekt: m.ClosureComponent = (vnode) => { const kundenOptionen: iSelectOption[] = getKunden().map((k: iKunde) => { return {value: k.id, label: k.name} }) - return m("div.w3-container.w3-auto", + return m("div.w3-container.w3-auto.form-view-container", m("h1", currentProjekt.id === -1 ? "Neues Projekt anlegen" : "Projekt-Stammblatt"), - m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [ + m("div", {style: {flex: '1', 'min-height': '0'}}, + m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [ m("label", "Kunde", m(Select, { name: "kundenId", @@ -232,7 +233,8 @@ export const Projekt: m.ClosureComponent = (vnode) => { m("label", "Zweck", m(Input, {name: "zweck", value: currentProjekt.zweck, type: "text"} as iInput)), m("label", "Verantwortlicher", m(Input, {name: "verantwortlicher", value: currentProjekt.verantwortlicher, type: "text"} as iInput)), m("label", "Startdatum", m(Input, {name: "startdatum", value: currentProjekt.startdatum, type: "date"} as iInput)) - ]) + ]) + ) ) } } diff --git a/Client/Components/tags.ts b/Client/Components/tags.ts index 37f114d..c7f71d8 100644 --- a/Client/Components/tags.ts +++ b/Client/Components/tags.ts @@ -526,29 +526,30 @@ export const Tag: m.ClosureComponent = (vnode) => { } }, view: (vnode) => { - return m("div.w3-container.w3-twothird", + return m("div.w3-container.w3-auto.form-view-container", m("h1", currentTag.id === -1 ? "Neuen Tag erstellen" : "Tag bearbeiten"), - m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [ + m("div", {style: {flex: '1', 'min-height': '0'}}, + m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [ m("label", "Tag-Name", m(Input, {name: "name", value: currentTag.name, type: "text"} as iInput)), m("label", "Beschreibung", m(Input, {name: "description", value: currentTag.description, type: "text"} as iInput)), - + // Eltern-Tag-Auswahl m("label", "Übergeordneter Tag (optional)", m("select.w3-select", {name: "parentId", value: currentTag.parentId || ""}, m("option", {value: ""}, "-- Kein übergeordneter Tag --"), - allTags.map(tag => + allTags.map(tag => m("option", {value: tag.id, selected: currentTag.parentId === tag.id}, tag.name) ) ) ), - + m("label", "Farbe"), - m("div", {style: {display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '12px'}}, - colorPalette.map(color => + m("div", {style: {display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '12px'}}, + colorPalette.map(color => m("button", { style: { - width: '32px', - height: '32px', + width: '32px', + height: '32px', backgroundColor: color, border: currentTag.color === color ? '2px solid black' : '1px solid #ccc', borderRadius: '4px', @@ -561,7 +562,8 @@ export const Tag: m.ClosureComponent = (vnode) => { m(Input, {name: "color", value: currentTag.color, type: "text"} as iInput), m("label", "Icon (optional)", m(Input, {name: "icon", value: currentTag.icon || "", type: "text"} as iInput)), m("label", "Sortierreihenfolge (optional)", m(Input, {name: "order", value: currentTag.order?.toString() || "", type: "number"} as iInput)), - ]) + ]) + ) ); } }; diff --git a/Client/Components/widgets/form.ts b/Client/Components/widgets/form.ts index 05ced96..5b701f8 100644 --- a/Client/Components/widgets/form.ts +++ b/Client/Components/widgets/form.ts @@ -6,6 +6,10 @@ export interface iForm { 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 } @@ -14,6 +18,8 @@ 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 @@ -74,6 +80,48 @@ export const Form:m.ClosureComponent = (vnode) => { 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'} + : {} + }, + [ + 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. + 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) + } + registerFormElement(form) + } + }, + // Scrollbarer Formular-Inhalt. + m("div", {style: {flex: '1', 'overflow-y': 'auto', 'padding-right': '8px'}}, + vnode.children + ), + // Fixierter Button-Bereich. + buttons + ) + } + // Ohne stickyButtons: bisheriges Verhalten (Buttons scrollen mit). return m("form", { onreset: () => {console.log("reset")}, onsubmit: submit, @@ -88,13 +136,7 @@ export const Form:m.ClosureComponent = (vnode) => { } }, vnode.children, - showButtons == true ? - m("div.w3-bar.w3-padding-16",[ - 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 + buttons ) } } diff --git a/NGINX_Server/html/static/app-theme.css b/NGINX_Server/html/static/app-theme.css index ff934d6..38fbcfe 100644 --- a/NGINX_Server/html/static/app-theme.css +++ b/NGINX_Server/html/static/app-theme.css @@ -19,4 +19,13 @@ .w3-hover-theme:hover {color:#fff !important; background-color:#9e9e9e !important} .w3-hover-text-theme:hover {color:#9e9e9e !important} -.w3-hover-border-theme:hover {border-color:#9e9e9e !important} \ No newline at end of file +.w3-hover-border-theme:hover {border-color:#9e9e9e !important} + +/* Container für Views, die ein Form-Widget mit stickyButtons enthalten: + voller Höhe, flex-Column, damit das Form-Widget den Inhalt scrollbar + hält und die Buttons fixiert am unteren Rand bleiben. */ +.form-view-container { + display: flex; + flex-direction: column; + height: 100%; +} \ No newline at end of file