Files
tsv08kulmbach-website/public/assets/js/form.js
fs 87055bd14b Design-System: Spacing auf Tokens migrieren
Alle padding/margin/gap-Deklarationen in components.css, layout.css und
base.css nutzen jetzt die Spacing-Skala — 414 Token-Verwendungen, kein roher
rem-Wert mehr in einer Abstands-Deklaration.

Die Substitution lief mechanisch (nächste Rasterstufe, Gleichstand aufgerundet),
damit sie deterministisch und nachvollziehbar ist statt 450-mal von Hand
geraten. Rund 325 Werte lagen bereits exakt auf einer Stufe und sind
verlustfrei übernommen. 125 Werte verschieben sich:

  ±0,4–2,0px   106 Werte  (0.1/0.15/0.3/0.35/0.4/0.6/0.85/0.9/1.1/1.6rem …)
  ±4px          12 Werte  (2.25 -> 2.5, 2.75 -> 3rem)
  ±8px           8 Werte  (3.5 -> 4, 4.5 -> 5, 7.5 -> 7rem)

Die 8px-Sprünge treffen ausschließlich Sektionsabstände (padding-block großer
Bänder, margin-top von Blöcken), wo sie optisch nicht ins Gewicht fallen. Die
±4px-Gruppe ist 2.25rem, das exakt zwischen zwei Stufen lag.

em-Werte bleiben bewusst roh: sie skalieren mit der font-size (Button-Padding,
Badge-Innenabstände) und gehören damit nicht auf ein festes px-Raster. Nullen,
auto und Prozentwerte sind unangetastet, calc() wurde korrekt mitgezogen
(calc(var(--header-h) + var(--space-5xl))).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSsVdooFLgPA8gPFp7HwZU
2026-07-27 10:09:33 +02:00

122 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Formulare: Progressive Enhancement — ohne JS normales POST + Redirect.
Mit JS: Inline-Feldfehler (aria-describedby, Fokus aufs erste ungültige Feld),
fetch-Submit, Status in aria-live-Region, Button-Sperre während des Sendens. */
(function () {
'use strict';
var form = document.querySelector('.form');
if (!form || !window.fetch) {
return;
}
var status = form.querySelector('[data-form-status]');
var button = form.querySelector('button[type="submit"]');
var MESSAGES = {
success: 'Danke für deine Anfrage! Wir melden uns so schnell wie möglich bei dir.',
validation: 'Bitte prüfe deine Eingaben die markierten Felder brauchen noch etwas Aufmerksamkeit.',
ratelimit: 'Zu viele Anfragen in kurzer Zeit. Bitte versuche es in ein paar Minuten erneut.',
busy: 'Bei uns gehen gerade ungewöhnlich viele Anfragen ein. Bitte versuche es später noch einmal.',
mail: 'Deine Nachricht konnte gerade nicht versendet werden. Bitte versuche es später erneut.'
};
function errorEl(field) {
return field.id ? document.getElementById(field.id + '-error') : null;
}
function showFieldError(field) {
var msg = errorEl(field);
field.setAttribute('aria-invalid', 'true');
if (msg) {
msg.textContent = field.validationMessage;
msg.hidden = false;
}
}
function clearFieldError(field) {
var msg = errorEl(field);
field.removeAttribute('aria-invalid');
if (msg) {
msg.textContent = '';
msg.hidden = true;
}
}
Array.prototype.forEach.call(form.elements, function (field) {
field.addEventListener('input', function () { clearFieldError(field); });
field.addEventListener('change', function () { clearFieldError(field); });
});
/* Feldfehler der Server-Validierung übernehmen ({ "<feld-id>": "Meldung" }) —
dieselben Elemente wie beim 422-Re-Render ohne JS. */
function showServerErrors(fields) {
Object.keys(fields).forEach(function (id) {
var field = document.getElementById(id);
var msg = document.getElementById(id + '-error');
if (field) {
field.setAttribute('aria-invalid', 'true');
}
if (msg) {
msg.textContent = fields[id];
msg.hidden = false;
}
});
}
function clearAllFieldErrors() {
Array.prototype.forEach.call(form.elements, clearFieldError);
}
function show(type, text) {
var p = document.createElement('p');
p.className = type === 'success' ? 'form__success' : 'form__error';
p.textContent = text;
status.replaceChildren(p);
status.focus({ preventScroll: true });
status.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault();
var invalid = form.querySelectorAll(':invalid');
Array.prototype.forEach.call(invalid, showFieldError);
if (invalid.length) {
invalid[0].focus();
}
return;
}
event.preventDefault();
var buttonLabel = button.textContent;
button.disabled = true;
button.textContent = 'Wird gesendet …';
fetch(form.action, {
method: 'POST',
body: new FormData(form),
headers: { Accept: 'application/json' }
})
.then(function (response) { return response.json(); })
.then(function (data) {
clearAllFieldErrors();
if (data.ok) {
show('success', MESSAGES.success);
form.reset();
} else {
if (data.fields) {
showServerErrors(data.fields);
}
show('error', MESSAGES[data.error] || MESSAGES.mail);
}
})
.catch(function () {
show('error', MESSAGES.mail);
})
.finally(function () {
button.disabled = false;
button.textContent = buttonLabel;
});
});
})();