diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7ef332..197c8429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,36 @@ # MForm - REDAXO Addon für Modul-Input-Formulare -# -# Version 9.0.1 + +## Version 9.1.0 + +### Neu + +- **Tabs als ID-freie Implementierung** – `addTabElement()` funktioniert jetzt stabil in verschachtelten und dynamischen Kontexten (z. B. FlexRepeater, Fieldset, Collapse, Modal), ohne Bootstrap-ID-Verkabelung und ohne Kollisionen zwischen geklonten Instanzen. +- **Optionale Tab-Modernisierung** – Tab-Navigation kann optional als modernisierte Variante gerendert werden über `tab-style => 'modern'` bzw. `data-group-tab-style => 'modern'`. +- **Optionale vertikale Tab-Navigation** – Tabs können optional links neben dem Inhalt dargestellt werden über `tab-layout => 'vertical'` bzw. `data-group-tab-layout => 'vertical'` (inkl. Mobile-Fallback auf gestapelte Ansicht). +- **Font-Awesome-Icons je Tab** – Icon-Unterstützung über `tab-icon` bleibt erhalten und funktioniert weiterhin in den neuen Tab-Varianten. +- **Form Builder erweitert (Issue #403, 9.1 Scope)** – neue Palette-/Generator-Unterstützung für `addColorSwatchField()`, `addToggleCheckboxField()`, `addModalElement()` sowie Alert-Elemente (`addAlertInfo/Warning/Danger/Success`). + +### Verbesserungen + +- **ColorSwatch-UX im Form Builder** – zusätzliche Hilfe im Eigenschaften-Panel, Beispiel-Palette per Klick und erweiterte Options-Syntax für CSS-Klassen-Swatches mit optionaler Preview-Farbe (z. B. `.text-primary=Primaer CSS|#2f77bc`). +- **Palette-Suche im Form Builder** – Live-Filter über Feld- und Wrapper-Typen inkl. Alias-Suche (z. B. `color`, `alert`, `link`) und Leerzustand-Hinweis bei keinen Treffern. +- **Schneller Fokus auf die Suche** – `/` fokussiert direkt das Suchfeld in der Palette. +- **Bessere Übersicht bei langen Listen** – Feld- und Wrapper-Liste haben jetzt eine maximale Höhe mit eigenem Scrollbereich in der linken Palette. +- **Barrierefreiheit bei Tabs verbessert** – ARIA-Zustände und Verknüpfungen wurden für Standard-Tabs und Repeater-Tabs ergänzt (`aria-selected`, `aria-controls`, `aria-labelledby`). + +### Behoben + +- **Tab-Active-Class im Parser** – fehlendes Leerzeichen beim Anhängen von `active` korrigiert, damit bestehende Klassen nicht zu ungültigen Tokens zusammenlaufen. +- **Dokubeispiele korrigiert** – fehlerhafte Beispiele in der Wrapper-/Repeater-Doku angepasst (`addCheckboxField`-Parameter, `addTextAreaField`-Methodenname). + +### Kompatibilität + +- Standardverhalten bleibt unverändert: Ohne neue Attribute bleiben Tabs visuell und funktional wie bisher. +- Bestehende Module mit `addTabElement()` bleiben kompatibel. + +--- + +## Version 9.0.1 ### Behoben diff --git a/assets/css/flex-repeater.css b/assets/css/flex-repeater.css index f37662c0..db4e801c 100644 --- a/assets/css/flex-repeater.css +++ b/assets/css/flex-repeater.css @@ -548,7 +548,7 @@ body.rex-theme-dark .mfr-btn-copy { .mfr-item-body.form-horizontal .mfr-field-group .control-label, .mfr-nested-body.form-horizontal .mfr-field-group .control-label { - word-break: break-word; + overflow-wrap: anywhere; } /* Vertical / Inline Layout: Label oben, Feld darunter, volle Breite. */ diff --git a/assets/css/formbuilder.css b/assets/css/formbuilder.css index 22968bd9..cc41be1f 100644 --- a/assets/css/formbuilder.css +++ b/assets/css/formbuilder.css @@ -206,6 +206,35 @@ body.rex-theme-dark .mform-fb { padding: 0; } +.mform-fb__field-list[data-fb-palette] { + max-height: 52vh; + overflow-y: auto; + overflow-x: hidden; + padding-right: 4px; +} + +.mform-fb__field-list[data-fb-palette-wrap] { + max-height: 24vh; + overflow-y: auto; + overflow-x: hidden; + padding-right: 4px; +} + +.mform-fb__palette-search { + margin: 0 0 10px; +} + +.mform-fb__palette-search .form-control { + height: 34px; + font-size: 13px; +} + +.mform-fb__palette-empty { + margin: 8px 0 0; + color: var(--fb-text-faint); + font-size: 12px; +} + .mform-fb__pal-item { background: var(--fb-bg-item); border: 1px solid var(--fb-border-strong); @@ -486,6 +515,16 @@ body.rex-theme-dark .mform-fb { color: var(--fb-text-faint); } +.mform-fb__colorswatch-help { + overflow-wrap: anywhere; +} + +.mform-fb__colorswatch-help code { + display: inline; + white-space: normal; + overflow-wrap: anywhere; +} + /* ---- Code Output --------------------------------------------------------- */ .mform-fb__code-bar { display: flex; diff --git a/assets/css/list-widget.css b/assets/css/list-widget.css index 31f7f5dc..831afbc8 100644 --- a/assets/css/list-widget.css +++ b/assets/css/list-widget.css @@ -137,7 +137,7 @@ flex-direction: column; align-items: flex-start; gap: .45rem; - word-break: break-word; + overflow-wrap: anywhere; } .mform-list-widget.mform-list-widget-medialist.is-grid-view .mform-list-item-media { diff --git a/assets/css/mform.css b/assets/css/mform.css index 774b8ad9..9a2b792b 100755 --- a/assets/css/mform.css +++ b/assets/css/mform.css @@ -29,6 +29,116 @@ background: #fff; } +.mform .mform-tabs { + --mform-tab-nav-bg: #f7f9fc; + --mform-tab-nav-border: #dfe5ee; + --mform-tab-content-bg: #ffffff; + --mform-tab-content-border: #dfe5ee; + --mform-tab-link-color: #48586b; + --mform-tab-link-active-bg: #ffffff; + --mform-tab-link-active-color: #1d2e40; + --mform-tab-link-hover-bg: #eef3f8; +} + +.mform .mform-tabs.mform-tabs--modern > .nav-tabs { + border-bottom: 1px solid var(--mform-tab-nav-border); + background: var(--mform-tab-nav-bg); + padding: 6px 6px 0; +} + +.mform .mform-tabs.mform-tabs--modern > .nav-tabs > li > a { + border-radius: 8px 8px 0 0; + border: 1px solid transparent; + color: var(--mform-tab-link-color); + transition: background-color .2s ease, color .2s ease, border-color .2s ease; +} + +.mform .mform-tabs.mform-tabs--modern > .nav-tabs > li > a:hover, +.mform .mform-tabs.mform-tabs--modern > .nav-tabs > li > a:focus { + background: var(--mform-tab-link-hover-bg); + color: var(--mform-tab-link-active-color); +} + +.mform .mform-tabs.mform-tabs--modern > .nav-tabs > li.active > a, +.mform .mform-tabs.mform-tabs--modern > .nav-tabs > li.active > a:hover, +.mform .mform-tabs.mform-tabs--modern > .nav-tabs > li.active > a:focus { + background: var(--mform-tab-link-active-bg); + border-color: var(--mform-tab-content-border) var(--mform-tab-content-border) var(--mform-tab-link-active-bg); + color: var(--mform-tab-link-active-color); +} + +.mform .mform-tabs.mform-tabs--modern > .tab-content { + border: 1px solid var(--mform-tab-content-border); + border-top: 0; + background: var(--mform-tab-content-bg); +} + +.mform .mform-tabs.mform-tabs--vertical { + display: flex; + align-items: stretch; +} + +.mform .mform-tabs.mform-tabs--vertical > .nav-tabs { + width: 240px; + min-width: 240px; + border-bottom: 0; + border-right: 1px solid var(--mform-tab-nav-border); + background: var(--mform-tab-nav-bg); + padding: 8px; +} + +.mform .mform-tabs.mform-tabs--vertical > .nav-tabs > li { + float: none; + width: 100%; + margin-bottom: 4px; +} + +.mform .mform-tabs.mform-tabs--vertical > .nav-tabs > li > a { + margin-right: 0; + border-radius: 6px; + border: 1px solid transparent; + color: var(--mform-tab-link-color); +} + +.mform .mform-tabs.mform-tabs--vertical > .nav-tabs > li > a:hover, +.mform .mform-tabs.mform-tabs--vertical > .nav-tabs > li > a:focus { + background: var(--mform-tab-link-hover-bg); + color: var(--mform-tab-link-active-color); +} + +.mform .mform-tabs.mform-tabs--vertical > .nav-tabs > li.active > a, +.mform .mform-tabs.mform-tabs--vertical > .nav-tabs > li.active > a:hover, +.mform .mform-tabs.mform-tabs--vertical > .nav-tabs > li.active > a:focus { + background: var(--mform-tab-link-active-bg); + border-color: var(--mform-tab-content-border); + color: var(--mform-tab-link-active-color); +} + +.mform .mform-tabs.mform-tabs--vertical > .tab-content { + flex: 1; + border: 1px solid var(--mform-tab-content-border); + border-left: 0; + background: var(--mform-tab-content-bg); +} + +@media (max-width: 767px) { + .mform .mform-tabs.mform-tabs--vertical { + display: block; + } + + .mform .mform-tabs.mform-tabs--vertical > .nav-tabs { + width: auto; + min-width: 0; + border-right: 0; + border-bottom: 1px solid var(--mform-tab-nav-border); + } + + .mform .mform-tabs.mform-tabs--vertical > .tab-content { + border-left: 1px solid var(--mform-tab-content-border); + border-top: 0; + } +} + .mform .mform-tabs.rex-page-nav:last-child { margin-bottom: 0; } @@ -169,6 +279,17 @@ body.rex-theme-dark .mform .mform-tabs .nav-tabs { /*background-color: rgba(0, 0, 0, 0.15);*/ } +body.rex-theme-dark .mform .mform-tabs { + --mform-tab-nav-bg: #19222d; + --mform-tab-nav-border: #304050; + --mform-tab-content-bg: #202b35; + --mform-tab-content-border: #304050; + --mform-tab-link-color: #b9c7d6; + --mform-tab-link-active-bg: #202b35; + --mform-tab-link-active-color: #eef4fb; + --mform-tab-link-hover-bg: #243342; +} + body.rex-theme-dark .mform .mform-tabs .tab-content { background-color: #202b35; } @@ -309,6 +430,17 @@ body.rex-theme-dark .mform .custom-link.input-group.is-empty input.form-control[ } @media (prefers-color-scheme: dark) { + body.rex-has-theme:not(.rex-theme-light) .mform .mform-tabs { + --mform-tab-nav-bg: #19222d; + --mform-tab-nav-border: #304050; + --mform-tab-content-bg: #202b35; + --mform-tab-content-border: #304050; + --mform-tab-link-color: #b9c7d6; + --mform-tab-link-active-bg: #202b35; + --mform-tab-link-active-color: #eef4fb; + --mform-tab-link-hover-bg: #243342; + } + body.rex-has-theme:not(.rex-theme-light) .mform .mform-tabs .nav-tabs { /*background-color: rgba(0, 0, 0, 0.15);*/ } diff --git a/assets/js/formbuilder.js b/assets/js/formbuilder.js index 9d7edf2f..bf7acd9f 100644 --- a/assets/js/formbuilder.js +++ b/assets/js/formbuilder.js @@ -70,6 +70,8 @@ props: ['label', 'defaultValue', 'options', 'notice', 'cssClass', 'required'] }, checkbox: { label: 'Checkbox', method: 'addCheckboxField', props: ['label', 'defaultValue', 'options', 'notice', 'cssClass'] }, + togglecheckbox: { label: 'Toggle Checkbox', method: 'addToggleCheckboxField', + props: ['label', 'defaultValue', 'options', 'notice', 'cssClass'] }, checkboxgroup: { label: 'Checkbox Group', method: 'addCheckboxGroupField', props: ['label', 'defaultValue', 'options', 'cbgLayout', 'cbgMode', 'notice', 'cssClass'] }, hidden: { label: 'Hidden', method: 'addHiddenField', @@ -78,8 +80,18 @@ props: ['label'] }, description: { label: 'Description', method: 'addDescription', props: ['label'] }, + alertinfo: { label: 'Alert Info', method: 'addAlertInfo', + props: ['alertText'] }, + alertwarning: { label: 'Alert Warning', method: 'addAlertWarning', + props: ['alertText'] }, + alertdanger: { label: 'Alert Danger', method: 'addAlertDanger', + props: ['alertText'] }, + alertsuccess: { label: 'Alert Success', method: 'addAlertSuccess', + props: ['alertText'] }, html: { label: 'HTML-Block', method: 'addHtml', props: ['htmlContent'] }, + colorswatch: { label: 'Color Swatch', method: 'addColorSwatchField', + props: ['label', 'defaultValue', 'options', 'colorSwatchHelp', 'notice', 'cssClass'] }, // REDAXO core widgets media: { label: 'Media', method: 'addMediaField', props: ['label', 'category', 'mediaType', 'notice', 'cssClass'] }, @@ -105,9 +117,11 @@ 'repeaterOpen', 'repeaterCopyPaste', 'repeaterConfirmDelete', 'repeaterConfirmDeleteMsg', 'repeaterBtnText', 'repeaterBtnClass'] }, tab: { label: 'Tab', method: 'addTabElement', - props: ['label', 'tabPullRight'] }, + props: ['label', 'tabPullRight', 'tabIcon', 'tabStyle', 'tabLayout'] }, fieldset: { label: 'Fieldset', method: 'addFieldsetArea', - props: ['label'] } + props: ['label'] }, + modal: { label: 'Modal', method: 'addModalElement', + props: ['label', 'modalBtnClass', 'modalAlign'] } }; var state = []; @@ -123,6 +137,8 @@ var $canvas = document.querySelector('[data-fb-canvas]'); var $palette = document.querySelector('[data-fb-palette]'); var $paletteWrap = document.querySelector('[data-fb-palette-wrap]'); + var $paletteSearch = document.querySelector('[data-fb-palette-search]'); + var $paletteEmpty = document.querySelector('[data-fb-palette-empty]'); var $code = document.querySelector('[data-fb-code]'); var $output = document.querySelector('[data-fb-output]'); var $propsForm = document.querySelector('[data-fb-props-form]'); @@ -135,7 +151,7 @@ function makeItem(type) { var def = TYPES[type]; - return { + var item = { uid: 'fb-' + (Math.random().toString(36).slice(2, 8)), id: nextId++, type: type, @@ -158,6 +174,7 @@ cbgMode: 'checkbox', // HTML-Block htmlContent: '', + alertText: (type === 'alertinfo' || type === 'alertwarning' || type === 'alertdanger' || type === 'alertsuccess') ? 'Hinweis' : '', // CustomLink clTypeIntern: type === 'customlink' || type === 'customlinkmultiple', clTypeExtern: type === 'customlink' || type === 'customlinkmultiple', @@ -184,8 +201,15 @@ repeaterBtnClass: '', // Tab tabPullRight: false, - children: (type === 'repeater' || type === 'tab' || type === 'fieldset') ? [] : null + tabIcon: '', + tabStyle: '', + tabLayout: '', + // Modal + modalBtnClass: 'btn-default', + modalAlign: 'left', + children: (type === 'repeater' || type === 'tab' || type === 'fieldset' || type === 'modal') ? [] : null }; + return item; } function findItem(uid, list) { @@ -267,7 +291,7 @@ var head = document.createElement('div'); head.className = 'mform-fb__item-head'; - var hasChildren = item.type === 'repeater' || item.type === 'tab' || item.type === 'fieldset'; + var hasChildren = item.type === 'repeater' || item.type === 'tab' || item.type === 'fieldset' || item.type === 'modal'; var collapseBtn = hasChildren ? '' : ''; @@ -290,7 +314,7 @@ }); el.appendChild(head); - if (item.type === 'repeater' || item.type === 'tab' || item.type === 'fieldset') { + if (item.type === 'repeater' || item.type === 'tab' || item.type === 'fieldset' || item.type === 'modal') { var nested = document.createElement('div'); nested.className = 'mform-fb__nested'; nested.dataset.fbNested = item.uid; @@ -303,7 +327,7 @@ nested.style.display = 'none'; } if (item.children.length === 0) { - var hintWord = item.type === 'tab' ? 'Tab' : (item.type === 'fieldset' ? 'Fieldset' : 'Repeater'); + var hintWord = item.type === 'tab' ? 'Tab' : (item.type === 'fieldset' ? 'Fieldset' : (item.type === 'modal' ? 'Modal' : 'Repeater')); nested.innerHTML = '

Felder hierher ziehen oder ' + hintWord + ' oben anklicken und dann links ein Feld waehlen

'; } else { item.children.forEach(function (c) { nested.appendChild(renderItem(c, nextDepth)); }); @@ -379,10 +403,6 @@ var nestedEl = sample ? sample.closest('[data-fb-nested]') : null; if (nestedEl) targetDepth = parseInt(nestedEl.dataset.fbDepth, 10) || 0; } - if (clipboard.type === 'tab' && targetDepth > 0) { - alert('Tabs koennen nur auf der obersten Ebene eingefuegt werden.'); - return; - } if (wouldExceedRepeaterDepth(clipboard, targetDepth)) { alert('Mehr als ' + (MAX_REPEATER_DEPTH + 1) + ' Repeater-Ebenen werden nicht unterstuetzt.'); return; @@ -477,6 +497,71 @@ emitCode(); }); + $propsForm.addEventListener('click', function (e) { + var btn = e.target.closest('[data-fb-action="colorswatch-example"]'); + if (!btn || !activeItem || activeItem.type !== 'colorswatch') return; + activeItem.options = '#ffffff=Weiss\n#111111=Schwarz\n#2f77bc=Blau\n#e74c3c=Rot\n#27ae60=Gruen\n.text-primary=Primaer CSS|#2f77bc\n.text-muted=Gedaempft CSS|#6c757d'; + var optionsInput = $propsForm.querySelector('[data-fb-prop="options"]'); + if (optionsInput) optionsInput.value = activeItem.options; + emitCode(); + }); + + var TYPE_SEARCH_ALIASES = { + colorswatch: 'color swatch farbe palette css preview', + togglecheckbox: 'toggle switch umschalter', + customlink: 'url link intern extern mailto tel', + customlinkmultiple: 'url links mehrere intern extern mailto tel', + alertinfo: 'info hinweis nachricht', + alertwarning: 'warning warnung', + alertdanger: 'danger fehler', + alertsuccess: 'success erfolg', + repeater: 'repeat wiederholung liste', + fieldset: 'gruppe bereich', + tab: 'tabs reiter', + modal: 'dialog popup' + }; + + function paletteSearchText(li) { + var label = (li.textContent || '').toLowerCase(); + var type = String(li.dataset.type || '').toLowerCase(); + var aliases = TYPE_SEARCH_ALIASES[type] || ''; + return label + ' ' + type + ' ' + aliases; + } + + function applyPaletteFilter(term) { + var q = String(term || '').trim().toLowerCase(); + var visibleCount = 0; + + [$palette, $paletteWrap].forEach(function (listEl) { + if (!listEl) return; + listEl.querySelectorAll('.mform-fb__pal-item').forEach(function (li) { + var show = q === '' || paletteSearchText(li).indexOf(q) !== -1; + li.style.display = show ? '' : 'none'; + if (show) visibleCount++; + }); + }); + + if ($paletteEmpty) { + $paletteEmpty.style.display = visibleCount === 0 ? '' : 'none'; + } + } + + if ($paletteSearch) { + $paletteSearch.addEventListener('input', function () { + applyPaletteFilter($paletteSearch.value); + }); + + document.addEventListener('keydown', function (e) { + if (e.key === '/' && document.activeElement !== $paletteSearch) { + e.preventDefault(); + $paletteSearch.focus(); + $paletteSearch.select(); + } + }); + } + + applyPaletteFilter(''); + // ---- Sortable wiring ------------------------------------------------ function listFor(el) { @@ -515,11 +600,6 @@ } // New item from palette has no children, so subtree depth is 0; depthOf check above // is sufficient for palette inserts. - if (type === 'tab' && depthOf(evt.to) > 0) { - alert('Tabs koennen nur auf der obersten Ebene platziert werden.'); - setTimeout(doRender, 0); - return; - } var newItem = makeItem(type); toList.splice(evt.newIndex, 0, newItem); setTimeout(function () { doRender(); selectItem(newItem); }, 0); @@ -538,12 +618,6 @@ setTimeout(doRender, 0); return; } - if (item.type === 'tab' && depthOf(evt.to) > 0) { - alert('Tabs koennen nur auf der obersten Ebene platziert werden.'); - setTimeout(doRender, 0); - return; - } - removeItem(uid); toList.splice(evt.newIndex, 0, item); setTimeout(doRender, 0); @@ -588,18 +662,9 @@ if (!type) return; var newItem = makeItem(type); - // Tabs are top-level only (Fieldsets duerfen ueberall liegen). - if (type === 'tab') { - state.push(newItem); - renderCanvas(); - emitCode(); - selectItem(newItem); - return; - } - // If an active repeater/tab/fieldset is selected, attempt to insert as child. // Otherwise append at top level. - var canNestInto = activeItem && (activeItem.type === 'repeater' || activeItem.type === 'tab' || activeItem.type === 'fieldset'); + var canNestInto = activeItem && (activeItem.type === 'repeater' || activeItem.type === 'tab' || activeItem.type === 'fieldset' || activeItem.type === 'modal'); if (canNestInto) { // Determine builder-depth of the active container's children list. var nestedEl = $canvas.querySelector('[data-fb-nested="' + activeItem.uid + '"]'); @@ -731,6 +796,29 @@ }); } + function parseColorSwatches(raw) { + if (!raw) return []; + return raw.split('\n').map(function (line) { return line.trim(); }).filter(Boolean).map(function (line, idx) { + var eq = line.indexOf('='); + if (eq === -1) { + return { key: String(line), label: String(line), preview: null }; + } + var key = line.slice(0, eq).trim(); + var rest = line.slice(eq + 1).trim(); + var pipe = rest.indexOf('|'); + if (pipe === -1) { + return { key: key || String(idx + 1), label: rest || key || String(idx + 1), preview: null }; + } + var label = rest.slice(0, pipe).trim(); + var preview = rest.slice(pipe + 1).trim(); + return { + key: key || String(idx + 1), + label: label || key || String(idx + 1), + preview: preview || null + }; + }); + } + function optionsArray(items) { var pairs = items.map(function (o) { var k = /^\d+$/.test(String(o.key)) ? String(o.key) : phpStr(o.key); @@ -739,6 +827,17 @@ return '[' + pairs.join(', ') + ']'; } + function colorSwatchArray(items) { + var pairs = items.map(function (o) { + var k = /^\d+$/.test(String(o.key)) ? String(o.key) : phpStr(o.key); + if (o.preview) { + return k + ' => [' + phpStr('label') + ' => ' + phpStr(o.label) + ', ' + phpStr('preview') + ' => ' + phpStr(o.preview) + ']'; + } + return k + ' => ' + phpStr(o.label); + }); + return '[' + pairs.join(', ') + ']'; + } + function slugify(s, fallback) { if (!s) return fallback; var slug = String(s).toLowerCase() @@ -813,6 +912,9 @@ if (item.type === 'html') { return def.method + '(' + phpStr(item.htmlContent || '') + ')'; } + if (item.type === 'alertinfo' || item.type === 'alertwarning' || item.type === 'alertdanger' || item.type === 'alertsuccess') { + return def.method + '(' + phpStr(item.alertText || '') + ')'; + } var idLit = typeof idArg === 'number' ? String(idArg) : phpStr(idArg); // Select Multiple: nutzt eine andere Methode, sonst gleiche Signatur wie select. var method = def.method; @@ -839,6 +941,21 @@ if (hasDef) line += ', ' + phpStr(item.defaultValue); break; } + case 'togglecheckbox': + case 'colorswatch': { + line += ', ' + colorSwatchArray(parseColorSwatches(item.options)); + var hasDefaultColorValue = !!item.defaultValue; + if (attrPhp || hasDefaultColorValue) line += ', ' + (attrPhp || 'null'); + if (hasDefaultColorValue) line += ', ' + phpStr(item.defaultValue); + break; + } + case 'togglecheckbox': { + line += ', ' + optionsArray(parseOptions(item.options)); + var hasDefaultValue = !!item.defaultValue; + if (attrPhp || hasDefaultValue) line += ', ' + (attrPhp || 'null'); + if (hasDefaultValue) line += ', ' + phpStr(item.defaultValue); + break; + } case 'checkboxgroup': // Signature: addCheckboxGroupField(id, options?, attributes?) // (default-value via 'default-value' in attributes, handled in attrsForItem) @@ -925,11 +1042,38 @@ return indent + '->addFieldsetArea(' + phpStr(legend) + ', MForm::factory()\n' + inner + ')'; } + function renderModalStmt(item, indent) { + var inner = renderRepeaterInner(item, indent); + var label = item.label || ''; + var btnClass = item.modalBtnClass || 'btn-default'; + var align = item.modalAlign || 'left'; + return indent + '$mform->addModalElement(' + phpStr(label) + ', MForm::factory()\n' + + inner + + indent + ' , ' + phpStr(btnClass) + ', ' + phpStr(align) + ');'; + } + + function renderInnerModalChainLink(item, indent) { + var inner = renderRepeaterInner(item, indent); + var label = item.label || ''; + var btnClass = item.modalBtnClass || 'btn-default'; + var align = item.modalAlign || 'left'; + return indent + '->addModalElement(' + phpStr(label) + ', MForm::factory()\n' + + inner + + indent + ' , ' + phpStr(btnClass) + ', ' + phpStr(align) + ')'; + } + function renderTabStmt(item, indent, isFirst) { var inner = renderRepeaterInner(item, indent); var label = item.label || ''; var openTab = isFirst ? 'true' : 'false'; var args = phpStr(label) + ', MForm::factory()\n' + inner; + var tabAttrs = tabAttrsPhp(item); + if (tabAttrs) { + if (item.tabPullRight) { + return indent + '$mform->addTabElement(' + args + ', ' + openTab + ', true, ' + tabAttrs + ');'; + } + return indent + '$mform->addTabElement(' + args + ', ' + openTab + ', false, ' + tabAttrs + ');'; + } // Drop trailing args we can default; only emit pullRight if true if (item.tabPullRight) { return indent + '$mform->addTabElement(' + args + ', ' + openTab + ', true);'; @@ -940,6 +1084,32 @@ return indent + '$mform->addTabElement(' + args + ');'; } + function renderInnerTabChainLink(item, indent, isFirst) { + var inner = renderRepeaterInner(item, indent); + var label = item.label || ''; + var openTab = isFirst ? 'true' : 'false'; + var args = phpStr(label) + ', MForm::factory()\n' + inner; + var tabAttrs = tabAttrsPhp(item); + if (tabAttrs) { + return indent + '->addTabElement(' + args + ', ' + openTab + ', ' + (item.tabPullRight ? 'true' : 'false') + ', ' + tabAttrs + ')'; + } + if (item.tabPullRight) { + return indent + '->addTabElement(' + args + ', ' + openTab + ', true)'; + } + if (isFirst) { + return indent + '->addTabElement(' + args + ', true)'; + } + return indent + '->addTabElement(' + args + ')'; + } + + function tabAttrsPhp(item) { + var attrs = {}; + if (item.tabIcon) attrs['tab-icon'] = item.tabIcon; + if (item.tabStyle) attrs['tab-style'] = item.tabStyle; + if (item.tabLayout) attrs['tab-layout'] = item.tabLayout; + return attrsToPhp(attrs); + } + function renderFieldsetStmt(item, indent) { var inner = renderRepeaterInner(item, indent); var legend = item.label || ''; @@ -976,17 +1146,30 @@ return indent + ' // Felder hier ablegen\n'; } var keyPool = {}; + var prevWasTab = false; var lines = children.map(function (c) { if (c.type === 'fieldset') { + prevWasTab = false; return renderInnerFieldsetChainLink(c, indent + ' '); } + if (c.type === 'modal') { + prevWasTab = false; + return renderInnerModalChainLink(c, indent + ' '); + } + if (c.type === 'tab') { + var isFirstTab = !prevWasTab; + prevWasTab = true; + return renderInnerTabChainLink(c, indent + ' ', isFirstTab); + } var key = slugify(c.label, 'field_' + c.id); var base = key, n = 2; while (keyPool[key]) { key = base + '_' + n++; } keyPool[key] = true; if (c.type === 'repeater') { + prevWasTab = false; return renderInnerRepeaterChainLink(c, key, indent + ' '); } + prevWasTab = false; return renderInnerChainLink(c, key, indent + ' '); }); return lines.join('\n') + '\n'; @@ -1017,6 +1200,8 @@ lines.push(renderTabStmt(item, '', isFirstTab)); } else if (item.type === 'fieldset') { lines.push(renderFieldsetStmt(item, '')); + } else if (item.type === 'modal') { + lines.push(renderModalStmt(item, '')); } else { lines.push(renderField(item, item.id, '') + ';'); } @@ -1089,6 +1274,7 @@ case 'imagelist': case 'checkbox': case 'checkboxgroup': + case 'togglecheckbox': // kommagetrennte Listen -> Array (laut Doku: array_filter(explode(...))) return 'array_filter(explode(",", ' + rv + '))'; case 'select': @@ -1122,6 +1308,7 @@ case 'imagelist': case 'checkbox': case 'checkboxgroup': + case 'togglecheckbox': return 'array_filter(explode(",", (string) (' + access + ')))'; case 'select': if (child.isMulti) { @@ -1157,7 +1344,7 @@ // Wenn select/radio/checkbox Optionen hat, zeige die moeglichen Werte // inkl. Labels als Kommentar (hilft beim Mapping in der Ausgabe). function optionValuesComment(item) { - if (item.type !== 'select' && item.type !== 'radio' && item.type !== 'checkbox' && item.type !== 'checkboxgroup') { + if (item.type !== 'select' && item.type !== 'radio' && item.type !== 'checkbox' && item.type !== 'checkboxgroup' && item.type !== 'togglecheckbox' && item.type !== 'colorswatch') { return []; } var opts = parseOptions(item.options || ''); @@ -1182,10 +1369,12 @@ case 'customlink': return 'normalisiertes Array mit customlink_url, customlink_text, customlink_target, customlink_class'; case 'customlinkmultiple': return 'Array normalisierter Custom-Links (je Eintrag customlink_url/_text/_target/_class)'; case 'checkbox': return 'Array der ausgewaehlten Werte (kommasepariert gespeichert)'; + case 'togglecheckbox': return 'Array der ausgewaehlten Werte (Toggle-Checkbox; meist ["1"] bei aktiv)'; case 'checkboxgroup': return item.cbgMode === 'radio' ? 'einzelner ausgewaehlter Wert (CheckboxGroup im Radio-Mode)' : 'Array der ausgewaehlten Werte (kommasepariert gespeichert)'; case 'select': return item.isMulti ? 'Array der ausgewaehlten Werte (Multi-Select, kommasepariert gespeichert)' : 'einzelner ausgewaehlter Wert'; case 'radio': return 'einzelner ausgewaehlter Wert'; + case 'colorswatch': return 'gewaehlter Farbwertausdruck (z. B. #2f77bc oder .text-primary)'; case 'textarea': return item.tinymce ? 'HTML aus dem Editor' : 'roher Text mit Zeilenumbruechen'; case 'hidden': return 'verstecktes Feld'; default: return ''; @@ -1217,7 +1406,7 @@ var inner = indent + ' '; var usedKeys = {}; // Fieldsets innerhalb des Repeaters sind reine UI-Wrapper -> Kinder hochziehen. - flattenFieldsetChildren(item.children || []).forEach(function (entry) { + flattenStructuralChildren(item.children || []).forEach(function (entry) { if (entry.kind === 'fs-marker') { lines.push(inner + entry.text); return; @@ -1232,7 +1421,7 @@ } lines.push(sub); delete c.__childKey; - } else if (c.type === 'headline' || c.type === 'description' || c.type === 'html') { + } else if (isStructureOnlyItem(c.type)) { // ueberspringen } else { var clbl = c.label ? ' (' + c.label + ')' : ''; @@ -1254,13 +1443,27 @@ // Output-Code werden sie aufgeloest. Marker-Kommentare bleiben aber als // visueller Trenner erhalten. Verschachtelte Fieldsets werden rekursiv // entpackt. - function flattenFieldsetChildren(children) { + function isAlertType(type) { + return type === 'alertinfo' || type === 'alertwarning' || type === 'alertdanger' || type === 'alertsuccess'; + } + + function isStructureOnlyItem(type) { + return type === 'headline' || type === 'description' || type === 'html' || isAlertType(type); + } + + function flattenStructuralChildren(children) { var out = []; (children || []).forEach(function (c) { if (c.type === 'fieldset') { var legend = c.label ? ': ' + c.label : ''; out.push({ kind: 'fs-marker', text: '// ----- Fieldset' + legend + ' -----' }); - flattenFieldsetChildren(c.children || []).forEach(function (e) { out.push(e); }); + flattenStructuralChildren(c.children || []).forEach(function (e) { out.push(e); }); + return; + } + if (c.type === 'modal') { + var modalLabel = c.label ? ': ' + c.label : ''; + out.push({ kind: 'fs-marker', text: '// ----- Modal' + modalLabel + ' -----' }); + flattenStructuralChildren(c.children || []).forEach(function (e) { out.push(e); }); return; } out.push({ kind: 'item', item: c }); @@ -1293,13 +1496,13 @@ } else { body.push('// ----- Tab -----'); } - flattenFieldsetChildren(item.children || []).forEach(function (entry) { + flattenStructuralChildren(item.children || []).forEach(function (entry) { if (entry.kind === 'fs-marker') { body.push(entry.text); return; } var c = entry.item; if (c.type === 'repeater') { body.push(renderRepeaterBlock(c, '')); body.push(''); - } else if (c.type === 'headline' || c.type === 'description' || c.type === 'html') { + } else if (isStructureOnlyItem(c.type)) { // ueberspringen (sind reine Form-Strukturhinweise) } else { body.push(renderTopLevelVar(c)); @@ -1315,20 +1518,39 @@ } else { body.push('// ----- Fieldset -----'); } - flattenFieldsetChildren(item.children || []).forEach(function (entry) { + flattenStructuralChildren(item.children || []).forEach(function (entry) { + if (entry.kind === 'fs-marker') { body.push(entry.text); return; } + var c = entry.item; + if (c.type === 'repeater') { + body.push(renderRepeaterBlock(c, '')); + body.push(''); + } else if (isStructureOnlyItem(c.type)) { + // ueberspringen + } else { + body.push(renderTopLevelVar(c)); + body.push(''); + } + }); + } else if (item.type === 'modal') { + if (item.label) { + body.push('// ----- Modal: ' + item.label + ' -----'); + } else { + body.push('// ----- Modal -----'); + } + flattenStructuralChildren(item.children || []).forEach(function (entry) { if (entry.kind === 'fs-marker') { body.push(entry.text); return; } var c = entry.item; if (c.type === 'repeater') { body.push(renderRepeaterBlock(c, '')); body.push(''); - } else if (c.type === 'headline' || c.type === 'description' || c.type === 'html') { + } else if (isStructureOnlyItem(c.type)) { // ueberspringen } else { body.push(renderTopLevelVar(c)); body.push(''); } }); - } else if (item.type === 'headline' || item.type === 'description' || item.type === 'html') { + } else if (isStructureOnlyItem(item.type)) { // ueberspringen } else { body.push(renderTopLevelVar(item)); diff --git a/assets/mform.js b/assets/mform.js index 8477135a..3491d333 100644 --- a/assets/mform.js +++ b/assets/mform.js @@ -190,14 +190,84 @@ function initMFormSelectPicker(mform) { function initMFormTabs(mform) { mform.find('.mform-tabs').each(function () { - let wrapper = $(this); - $(this).find('ul[role=tablist] a').unbind().bind('click', function () { - let tab = wrapper.find('div[data-tab-group-nav-tab-id=' + $(this).data('tab-item') + ']'), - uid = Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); - tab.attr('id', uid); - $(this).attr('href', '#' + uid); - $('#' + uid).tab("show"); + let wrapper = $(this), + nav = wrapper.children('ul[role=tablist]').first(), + panes = wrapper.children('.tab-content').first().children('.tab-pane'); + + if (!nav.length || !panes.length) { + return; + } + + function ensureAriaLinking() { + let baseId = 'mform-tab-' + Math.random().toString(36).slice(2, 10); + + nav.find('[data-mform-tab-toggle]').each(function (idx) { + let link = $(this), + tabId = String(link.data('tab-item')), + pane = panes.filter(function () { + return String($(this).attr('data-tab-group-nav-tab-id')) === tabId; + }).first(); + + if (!pane.length) { + return; + } + + let linkId = link.attr('id'); + if (!linkId) { + linkId = baseId + '-tab-' + idx; + link.attr('id', linkId); + } + + let paneId = pane.attr('id'); + if (!paneId) { + paneId = baseId + '-panel-' + idx; + pane.attr('id', paneId); + } + + link.attr('aria-controls', paneId); + pane.attr('aria-labelledby', linkId); + }); + } + + function activateTab(tabId) { + let links = nav.find('[data-mform-tab-toggle]'), + targetLink = links.filter(function () { + return String($(this).data('tab-item')) === String(tabId); + }).first(); + + if (!targetLink.length) { + targetLink = links.first(); + tabId = String(targetLink.data('tab-item')); + } + + nav.find('li[role=presentation]').removeClass('active'); + links.attr('aria-selected', 'false'); + targetLink.closest('li[role=presentation]').addClass('active'); + targetLink.attr('aria-selected', 'true'); + + panes.removeClass('active'); + panes.filter(function () { + return String($(this).attr('data-tab-group-nav-tab-id')) === String(tabId); + }).first().addClass('active'); + } + + wrapper.off('click.mformTabs').on('click.mformTabs', '[data-mform-tab-toggle]', function (e) { + if ($(this).closest('ul[role=tablist]')[0] !== nav[0]) { + return; + } + e.preventDefault(); + e.stopPropagation(); + activateTab($(this).data('tab-item')); }); + + ensureAriaLinking(); + + let activeLink = nav.find('li.active [data-mform-tab-toggle]').first(); + if (activeLink.length) { + activateTab(activeLink.data('tab-item')); + } else { + activateTab(nav.find('[data-mform-tab-toggle]').first().data('tab-item')); + } }); } diff --git a/assets/repeater.js b/assets/repeater.js index 0a279833..e392de70 100644 --- a/assets/repeater.js +++ b/assets/repeater.js @@ -66,14 +66,84 @@ window.repeater = () => { let tabs = $(element).find('.mform-tabs'); if (tabs.length > 0) { tabs.each(function() { - let wrapper = $(this); - $(this).find('ul[role=tablist] a').unbind().bind('click', function() { - let tab = wrapper.find('div[data-tab-group-nav-tab-id=' + $(this).data('tab-item') + ']'), - uid = Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); - tab.attr('id', uid); - $(this).attr('href', '#' + uid); - $('#' + uid).tab("show"); + let wrapper = $(this), + nav = wrapper.children('ul[role=tablist]').first(), + panes = wrapper.children('.tab-content').first().children('.tab-pane'); + + if (!nav.length || !panes.length) { + return; + } + + function ensureAriaLinking() { + let baseId = 'mform-r-tab-' + Math.random().toString(36).slice(2, 10); + + nav.find('[data-mform-tab-toggle]').each(function (idx) { + let link = $(this), + tabId = String(link.data('tab-item')), + pane = panes.filter(function () { + return String($(this).attr('data-tab-group-nav-tab-id')) === tabId; + }).first(); + + if (!pane.length) { + return; + } + + let linkId = link.attr('id'); + if (!linkId) { + linkId = baseId + '-tab-' + idx; + link.attr('id', linkId); + } + + let paneId = pane.attr('id'); + if (!paneId) { + paneId = baseId + '-panel-' + idx; + pane.attr('id', paneId); + } + + link.attr('aria-controls', paneId); + pane.attr('aria-labelledby', linkId); + }); + } + + function activateTab(tabId) { + let links = nav.find('[data-mform-tab-toggle]'), + targetLink = links.filter(function () { + return String($(this).data('tab-item')) === String(tabId); + }).first(); + + if (!targetLink.length) { + targetLink = links.first(); + tabId = String(targetLink.data('tab-item')); + } + + nav.find('li[role=presentation]').removeClass('active'); + links.attr('aria-selected', 'false'); + targetLink.closest('li[role=presentation]').addClass('active'); + targetLink.attr('aria-selected', 'true'); + + panes.removeClass('active'); + panes.filter(function () { + return String($(this).attr('data-tab-group-nav-tab-id')) === String(tabId); + }).first().addClass('active'); + } + + wrapper.off('click.mformTabs').on('click.mformTabs', '[data-mform-tab-toggle]', function (e) { + if ($(this).closest('ul[role=tablist]')[0] !== nav[0]) { + return; + } + e.preventDefault(); + e.stopPropagation(); + activateTab($(this).data('tab-item')); }); + + ensureAriaLinking(); + + let activeLink = nav.find('li.active [data-mform-tab-toggle]').first(); + if (activeLink.length) { + activateTab(activeLink.data('tab-item')); + } else { + activateTab(nav.find('[data-mform-tab-toggle]').first().data('tab-item')); + } }); } }, diff --git a/docs/00_whats_new.md b/docs/00_whats_new.md index dac3443d..bae2842d 100644 --- a/docs/00_whats_new.md +++ b/docs/00_whats_new.md @@ -4,6 +4,15 @@ MForm 9 ist ein umfassendes Upgrade mit neuen Feldern, einem vollständig neuen --- +## Update 9.1: Tabs und Form Builder + +- Tabs werden ID-frei gerendert und sind dadurch stabil in verschachtelten Kontexten (inkl. FlexRepeater). +- `addTabElement()` kann direkt in Repeater-Item-Formularen genutzt werden. +- Optional pro Tab: `tab-icon`, `tab-style => modern`, `tab-layout => vertical`. +- Der Visual Form Builder unterstuetzt diese drei Tab-Optionen ebenfalls in den Tab-Eigenschaften. + +--- + ## Fixes & Verbesserungen in beta4 ### Flex-Repeater: Alle Link- und Media-Widgets vollständig unterstützt diff --git a/docs/05_wrapper.md b/docs/05_wrapper.md index 275bfbc9..b690e335 100644 --- a/docs/05_wrapper.md +++ b/docs/05_wrapper.md @@ -190,6 +190,8 @@ echo $mform->show(); Stellt Tab-Elemente dar, die bei Klick den dargestellten Inhalt wechseln. +Optional kann die Darstellung modernisiert oder vertikal (Navigation links) ausgegeben werden. + ```php show(); ``` +### Optionale Tab-Varianten + +```php +addTabElement('Inhalt', MForm::factory() + ->addTextField('1.0.1', ['label' => 'Titel']) + , true, false, [ + 'tab-icon' => 'fa-file-text-o', + 'tab-style' => 'modern', + 'tab-layout' => 'vertical', + ]) + ->addTabElement('Einstellungen', MForm::factory() + ->addCheckboxField('1.0.2', [1 => 'Aktiv'], ['label' => 'Status']) + , false, false, [ + 'tab-icon' => 'fa-cog', + ]); + +echo $mform->show(); +``` + +Hinweise: + +- `tab-style => 'modern'` ist optional und aktiviert eine modernisierte Optik. +- `tab-layout => 'vertical'` ist optional und zeigt die Tab-Navigation links neben dem Content. +- Alternativ koennen die Roh-Attribute `data-group-tab-style` und `data-group-tab-layout` gesetzt werden. + ## Modal Öffnet ein Bootstrap-Modal mit einem Sub-Formular. Ein Button wird direkt im Formular eingebettet; alle Felder im Modal-Inhalt werden beim Speichern des Moduls normal übernommen – kein separater AJAX-Request erforderlich. diff --git a/docs/07_repeater.md b/docs/07_repeater.md index 4ae88e48..453f450b 100644 --- a/docs/07_repeater.md +++ b/docs/07_repeater.md @@ -273,11 +273,11 @@ Verfügbare Optionen im Repeater-Array: ## Wrapper im Repeater (Tabs, Collapse, Fieldset, Inline, Columns) -Seit **9.0.0** rendert der FlexRepeater alle gaengigen MForm-Wrapper auch innerhalb eines Repeater-Items. Damit lassen sich komplexe Item-Layouts wie im klassischen MForm-Pfad aufbauen – inkl. Bootstrap-3-Tabs, Collapse-Gruppen, Fieldsets mit Legend, Form-Inline und Spalten-Grids. +Seit **9.0.0** rendert der FlexRepeater alle gaengigen MForm-Wrapper auch innerhalb eines Repeater-Items. Damit lassen sich komplexe Item-Layouts wie im klassischen MForm-Pfad aufbauen – inkl. Tabs, Collapse-Gruppen, Fieldsets mit Legend, Form-Inline und Spalten-Grids. | Wrapper | Methode | Markup im Repeater-Item | |---------|---------|-------------------------| -| Tabs | `addStartGroupTab()` / `addTab()` / `addCloseTab()` / `addCloseGroupTab()` | Bootstrap-3 `nav-tabs` + `tab-content` | +| Tabs | `addStartGroupTab()` / `addTab()` / `addCloseTab()` / `addCloseGroupTab()` | ID-freie `nav-tabs` + `tab-content` (scoped pro Wrapper) | | Collapse (mit Toggle-Button) | `addCollapseElement()` | `` + `.collapse[data-group-collapse-id=…]` | | Collapse-Gruppe (Standalone-Toggle) | `addStartGroupCollapse()` / `addCloseGroupCollapse()` | `.collapse-group[data-group-accordion=0\|1]` | | Fieldset | `addFieldsetArea($legend, …)` | `
` | @@ -287,7 +287,29 @@ Seit **9.0.0** rendert der FlexRepeater alle gaengigen MForm-Wrapper auch innerh ### Tabs im Repeater -Tab-Panel-IDs muessen pro Item-Klon eindeutig sein. Der Renderer schreibt deshalb Platzhalter `__MFRTAB___` in die Tab-`href`s, `aria-controls` und Tab-Pane-`id`s. `flex-repeater.js _renderItem()` ersetzt die Platzhalter beim Klonen jedes Items durch eine eindeutige UID (gleiche `n` → gleiche UID, damit Nav-Link und Pane matchen). Aktive Tabs werden ueber `data-group-open-tab => true` markiert (setzt `active` auf `
  • ` und `tab-pane`). +Tabs werden ID-frei gerendert. Die Navigation wird innerhalb des naechstgelegenen `.mform-tabs`-Wrappers gescoped und per `data-tab-item`/`data-tab-group-nav-tab-id` verknuepft. Dadurch funktionieren geklonte Repeater-Items und verschachtelte Tab-Strukturen ohne ID-Kollisionen. Aktive Tabs werden weiterhin ueber `data-group-open-tab => true` markiert. + +Empfohlener Weg seit 9.1: `addTabElement()` direkt im Repeater-Item-Formular. Falls keine explizite Tab-Gruppe (`addStartGroupTab()` / `addCloseGroupTab()`) vorhanden ist, gruppiert der FlexRepeater-Renderer die Tabs automatisch. + +```php +$itemForm = MForm::factory() + ->addTabElement('Inhalt', MForm::factory() + ->addTextField('title', ['label' => 'Titel']) + , true, false, [ + 'tab-icon' => 'fa-align-left', + 'tab-style' => 'modern', + ]) + ->addTabElement('Meta', MForm::factory() + ->addTextAreaField('text', ['label' => 'Text']) + , false, false, [ + 'tab-icon' => 'fa-cog', + 'tab-style' => 'modern', + ]); + +$mform->addRepeaterElement(1, $itemForm); +``` + +Alternative (weiterhin moeglich) ist die explizite Gruppierung mit `addStartGroupTab()` / `addTab()` / `addCloseTab()` / `addCloseGroupTab()`: ```php $itemForm = MForm::factory() diff --git a/docs/13_api_reference.md b/docs/13_api_reference.md index 87229885..620d4964 100644 --- a/docs/13_api_reference.md +++ b/docs/13_api_reference.md @@ -206,6 +206,7 @@ Inline-Layout-Container mit Label. addTabElement(string $label = '', mixed $form = null, bool $openTab = false, bool $pullNaviItemRight = false, array $attributes = [], bool $parse = false, bool $showWrapper = false): MForm ``` Tab-Panel. Mehrere Tabs werden automatisch zu einer Tab-Leiste zusammengefasst. +Funktioniert auch innerhalb von FlexRepeater-, Fieldset-, Collapse- und Modal-Kontexten. | Parameter | Typ | Standard | Beschreibung | |-----------|-----|----------|--------------| @@ -213,6 +214,14 @@ Tab-Panel. Mehrere Tabs werden automatisch zu einer Tab-Leiste zusammengefasst. | `$openTab` | `bool` | `false` | Tab initial geöffnet | | `$pullNaviItemRight` | `bool` | `false` | Tab-Item nach rechts schieben | +Optionale `$attributes` fuer Tabs: + +- `tab-icon` (z. B. `fa-cog`): Icon im Tab-Label. +- `tab-style => 'modern'` oder `data-group-tab-style => 'modern'`: modernisierte Tab-Optik. +- `tab-layout => 'vertical'` oder `data-group-tab-layout => 'vertical'`: vertikale Navigation links neben dem Tab-Content. + +Visual Form Builder (9.1): Fuer Tab-Elemente koennen `tab-icon`, `tab-style` und `tab-layout` direkt in den Eigenschaften gesetzt werden. + --- ```php diff --git a/docs/ROADMAP_9.1_ISSUES.md b/docs/ROADMAP_9.1_ISSUES.md index a8b0af8d..f945449e 100644 --- a/docs/ROADMAP_9.1_ISSUES.md +++ b/docs/ROADMAP_9.1_ISSUES.md @@ -53,17 +53,20 @@ Der Visual Form Builder (eingeführt in 9.0.0-beta6) deckt aktuell 16 Feldtypen ### Tasks +- 9.1 Fokus (priorisiert): - [ ] `addColorSwatchField()` – Color-Swatch (klein) - [ ] `addModalElement()` – Bootstrap-Modal als Sub-Form (mittel) +- [ ] `addToggleCheckboxField()` – Switch-Checkbox (klein) +- [ ] `addAlertInfo/Warning/Danger/Success` – Alert-Varianten als eigene Palette-Items (klein) + +- Nach 9.2 ausgelagert: siehe Issue [#425](https://github.com/FriendsOfREDAXO/mform/issues/425) - [ ] `addCollapseElement()` – Collapse-Container (mittel) - [ ] `addAccordionElement()` – Accordion-Container (mittel) - [ ] `addColumnElement()` – Bootstrap-Spalte (mittel) - [ ] `addInlineElement()` – Inline-Group (klein) -- [ ] `addToggleCheckboxField()` – Switch-Checkbox (klein) - [ ] `addMultiSelectField()` – Multi-Select (klein) - [ ] `addRadioImgField()`, `addRadioIconField()`, `addRadioColorField()` – visuelle Radios (mittel) - [ ] `addTextReadOnlyField()`, `addTextAreaReadOnlyField()` – Readonly-Varianten (klein) -- [ ] `addAlertInfo/Warning/Danger/Success` – Alert-Varianten als eigene Palette-Items (klein) - [ ] `addConditionalFieldsetArea()` – Conditional-Wrapper (groß; Properties-Panel mit Bedingungs-Editor) Pro Feldtyp: diff --git a/fragments/mform/mform_wrapper.php b/fragments/mform/mform_wrapper.php index 566bec13..cabb7f70 100644 --- a/fragments/mform/mform_wrapper.php +++ b/fragments/mform/mform_wrapper.php @@ -42,13 +42,14 @@ // TAB case 'tabnavli': - echo '
  • '; + $isActive = 1 === preg_match('/(^|\s)active(\s|$)/', (string) $this->getVar('class')); + echo ''; break; case 'tab': echo '
    getVar('attributes') . '>'; break; case 'start-group-tab': - echo '