Zwei Bugs im Scroll-zum-Fehler-Helper: '[wire\:submit]' matchte das <form> selbst, sodass jeder Klick in ein Feld den Scroll armierte (sichtbar beim USt-ID-Live-Check im Profil); und der leere, versteckte Flux-Error-Container unter jedem Feld galt wegen Whitespace als ':not(:empty)' — fokussiert wurde dadurch grundlos das erste Feld. Enter-Submit wird jetzt über das submit-Event erkannt, Fehlerfelder nur noch bei echtem Fehlertext. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
3.8 KiB
JavaScript
95 lines
3.8 KiB
JavaScript
/**
|
|
* Portal Form Hooks
|
|
*
|
|
* Globale UX-Helper für FluxUI-Forms im Hub/Portal-Bereich.
|
|
*
|
|
* Aktuell:
|
|
* 1) Smooth-Scroll zum ersten Validation-Error nach Submit-Klick,
|
|
* damit der User in langen Forms (z.B. PR-Edit) nicht nach Errors
|
|
* suchen muss.
|
|
*
|
|
* Wird im Portal-Layout NACH @fluxScripts eingebunden — Livewire ist
|
|
* dann garantiert verfügbar. Bewusst KEIN Alpine.start() o.ä.; FluxUI
|
|
* bringt seine eigene Alpine-Instanz mit, doppelter Bootstrap würde
|
|
* Komponenten brechen (siehe partials/head.blade.php Kommentar).
|
|
*/
|
|
|
|
(function () {
|
|
if (typeof document === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
// Pending-Flag: wird nur gesetzt, wenn der User explizit auf einen
|
|
// Submit-/Save-Button klickt. Andernfalls würde JEDES wire:model-Update
|
|
// einen Scroll triggern, was bei Live-Validation extrem nervig wäre.
|
|
let scrollPending = false;
|
|
|
|
// Selektoren für Buttons, die wir als "Submit-Intent" interpretieren.
|
|
// Bewusst NICHT '[wire\\:submit]': das matcht das <form> selbst, und da
|
|
// Klicks bubbeln, würde jeder Klick in irgendein Feld den Scroll armieren
|
|
// (Bug 08.07.: USt-ID-Live-Check sprang zum ersten Feld hoch).
|
|
const SUBMIT_SELECTORS = [
|
|
'[wire\\:click*="save"]',
|
|
'[wire\\:click*="submit"]',
|
|
'[wire\\:click*="update"]',
|
|
'button[type="submit"]',
|
|
].join(',');
|
|
|
|
document.addEventListener('click', (event) => {
|
|
const trigger = event.target.closest(SUBMIT_SELECTORS);
|
|
if (trigger) {
|
|
scrollPending = true;
|
|
}
|
|
}, true);
|
|
|
|
// Enter-Submit in wire:submit-Forms kommt nicht als Button-Klick an —
|
|
// das submit-Event deckt ihn ab (Livewire preventet nur den Default).
|
|
document.addEventListener('submit', (event) => {
|
|
if (event.target.matches('[wire\\:submit]')) {
|
|
scrollPending = true;
|
|
}
|
|
}, true);
|
|
|
|
document.addEventListener('livewire:init', () => {
|
|
if (!window.Livewire || typeof window.Livewire.hook !== 'function') {
|
|
return;
|
|
}
|
|
|
|
window.Livewire.hook('commit', ({ succeed }) => {
|
|
succeed(() => {
|
|
if (!scrollPending) {
|
|
return;
|
|
}
|
|
scrollPending = false;
|
|
|
|
requestAnimationFrame(() => {
|
|
// Flux rendert unter JEDEM Feld einen (hidden) Error-Container
|
|
// mit Whitespace-Inhalt — ':not(:empty)' matcht den daher auch
|
|
// ohne Fehler. Nur Container mit echtem Text zählen.
|
|
const invalid = document.querySelector('[data-flux-control][aria-invalid="true"]')
|
|
|| document.querySelector('[aria-invalid="true"]')
|
|
|| Array.from(document.querySelectorAll('[data-flux-error]'))
|
|
.find((el) => el.textContent.trim() !== '');
|
|
|
|
if (!invalid) {
|
|
return;
|
|
}
|
|
|
|
const field = invalid.closest('[data-flux-field]')
|
|
|| invalid.closest('[data-flux-control]')
|
|
|| invalid;
|
|
|
|
field.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
|
const focusable = field.querySelector('input, textarea, select, [contenteditable="true"]');
|
|
if (focusable && typeof focusable.focus === 'function') {
|
|
// Kleine Verzögerung, damit der Scroll erst sichtbar startet,
|
|
// bevor wir den Cursor reinpacken — sonst springt der Browser
|
|
// direkt zum Element und das smooth-Scroll wirkt unruhig.
|
|
setTimeout(() => focusable.focus({ preventScroll: true }), 320);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
})();
|