fix(ux): locale reload feedback, submit validation, dedup blur logic

- Locale-Picker: disable select + fade before location.reload() (system mode)
  gives visual feedback before the page jumps
- Extract _validateField() helper from wireBlurValidation to avoid duplication
- Add validateAll(formContainer): validates all required fields on demand,
  marks inline errors, focuses first invalid field
- tasks.js: call validateAll() at submit to catch untouched required fields
This commit is contained in:
Ulas
2026-04-05 12:33:57 +02:00
parent 44e5a879b9
commit a5ae0bac7e
3 changed files with 38 additions and 8 deletions
+30 -6
View File
@@ -484,21 +484,45 @@ export function confirmModal(message, { confirmLabel, danger = false } = {}) {
// Inline Blur-Validierung // Inline Blur-Validierung
// -------------------------------------------------------- // --------------------------------------------------------
function _validateField(input) {
const group = input.closest('.form-field') ?? input.parentElement;
const hasValue = input.value.trim().length > 0;
group?.classList.toggle('form-field--error', !hasValue);
group?.classList.toggle('form-field--valid', hasValue);
return hasValue;
}
/** /**
* Aktiviert Blur-Validierung für alle required-Inputs in einem Container. * Aktiviert Blur-Validierung für alle required-Inputs in einem Container.
* @param {HTMLElement} formContainer * @param {HTMLElement} formContainer
*/ */
export function wireBlurValidation(formContainer) { export function wireBlurValidation(formContainer) {
formContainer.querySelectorAll('input[required], select[required], textarea[required]').forEach((input) => { formContainer.querySelectorAll('input[required], select[required], textarea[required]').forEach((input) => {
input.addEventListener('blur', () => { input.addEventListener('blur', () => _validateField(input));
const group = input.closest('.form-field') ?? input.parentElement;
const hasValue = input.value.trim().length > 0;
group?.classList.toggle('form-field--error', !hasValue);
group?.classList.toggle('form-field--valid', hasValue);
});
}); });
} }
/**
* Validiert alle required-Inputs sofort (z.B. beim Submit ohne vorangehendes Blur).
* Markiert Fehler inline und fokussiert das erste ungültige Feld.
*
* @param {HTMLElement} formContainer
* @returns {boolean} true wenn alle Felder valide sind
*/
export function validateAll(formContainer) {
let firstInvalid = null;
let allValid = true;
formContainer.querySelectorAll('input[required], select[required], textarea[required]').forEach((input) => {
const valid = _validateField(input);
if (!valid && !firstInvalid) firstInvalid = input;
if (!valid) allValid = false;
});
if (firstInvalid) firstInvalid.focus();
return allValid;
}
// -------------------------------------------------------- // --------------------------------------------------------
// Submit-Feedback (Checkmark + Shake) // Submit-Feedback (Checkmark + Shake)
// -------------------------------------------------------- // --------------------------------------------------------
+4 -1
View File
@@ -55,8 +55,11 @@ class OikosLocalePicker extends HTMLElement {
select.addEventListener('change', () => { select.addEventListener('change', () => {
if (select.value === 'system') { if (select.value === 'system') {
select.disabled = true;
select.style.opacity = '0.5';
localStorage.removeItem('oikos-locale'); localStorage.removeItem('oikos-locale');
location.reload(); // Kurze Verzögerung damit der Browser den disabled-Zustand rendert
setTimeout(() => location.reload(), 60);
} else { } else {
setLocale(select.value); setLocale(select.value);
} }
+4 -1
View File
@@ -6,7 +6,7 @@
import { api } from '/api.js'; import { api } from '/api.js';
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js'; import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
import { openModal as openSharedModal, closeModal, wireBlurValidation, btnSuccess, btnError, promptModal, confirmModal } from '/components/modal.js'; import { openModal as openSharedModal, closeModal, wireBlurValidation, validateAll, btnSuccess, btnError, promptModal, confirmModal } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js'; import { stagger, vibrate } from '/utils/ux.js';
import { t, formatDate } from '/i18n.js'; import { t, formatDate } from '/i18n.js';
import { esc } from '/utils/html.js'; import { esc } from '/utils/html.js';
@@ -430,6 +430,9 @@ async function handleFormSubmit(e, container) {
const submitBtn = document.getElementById('task-submit-btn'); const submitBtn = document.getElementById('task-submit-btn');
const taskId = document.getElementById('task-id').value; const taskId = document.getElementById('task-id').value;
// Alle required-Felder sofort validieren (auch unberührte)
if (!validateAll(form)) return;
errorEl.hidden = true; errorEl.hidden = true;
submitBtn.disabled = true; submitBtn.disabled = true;
submitBtn.textContent = t('common.saving'); submitBtn.textContent = t('common.saving');