Files

76 lines
3.9 KiB
PHP
Raw Permalink Normal View History

<?php
declare(strict_types=1);
/**
* Einheitliches Formularfeld EINZIGE Quelle für Feld-Markup aller Formulare.
*
* Props: $field (array):
Turn-Anfrageformular: Feld „Geburtsdatum des Kindes" wieder da Auf der alten Seite gab es das Feld, beim Neubau ist es untergegangen. Gegen 2024.tsv08kulmbach.de geprüft: dort hieß es input_alter_kind, war type="date", OPTIONAL und trug das Label „Geburtsdatum des Kindes" — genau so wieder eingebaut. Die Abteilung braucht es, weil die Gruppen nach Alter eingeteilt sind. Nur in der Turn-Variante. In der allgemeinen wäre es sinnlos und würde ohne Not personenbezogene Daten Minderjähriger erheben. Dafür drei Stellen: - contact_variant(): Schalter ask_birthdate (true nur für turnen) - contact-form.php: Feld definiert, bedingt gerendert, direkt nach der Gruppenauswahl wie auf der alten Seite - contact-submit.php: Validierung UND Rücksetzen auf leer, wenn die Variante das Feld nicht anbietet — sonst könnte ein manipulierter POST es an der allgemeinen Variante vorbei einschmuggeln Validierung serverseitig, nicht nur per type=date: echtes Datum, nicht in der Zukunft, nicht vor 1900. createFromFormat mit Rückvergleich, weil PHP sonst Scheindaten wie 2019-02-31 stillschweigend auf den 3. März verschiebt. Geprüft mit ausreichend altem Token (die Time-Trap verlangt 3 s, meine ersten Versuche liefen deshalb als Bot durch): 2019-02-31 abgewiesen, 2030-01-01 abgewiesen, gültiges Datum passiert. Ohne JS bleibt der Wert bei HTTP 422 erhalten (value="2019-03-12" im Re-Render nachgewiesen). Kein neues Markup: form-field kann type=date und min/max längst, die Prop für den Hilfetext heißt hint (nicht help, das war mein erster Fehlgriff). Docblock der Komponente um date und min/max ergänzt. Datenschutzerklärung mitgezogen, wie die Projektregel es für neue Formularfelder verlangt — mit Zweckbindung (Zuordnung zur Altersgruppe) und dem ausdrücklichen Hinweis, dass es keine Pflichtangabe ist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:10:47 +02:00
* type text | email | tel | date | select | textarea | checkbox (Default: text)
* name, label Pflicht
* id Default: field-<name>
* required bool
Turn-Anfrageformular: Feld „Geburtsdatum des Kindes" wieder da Auf der alten Seite gab es das Feld, beim Neubau ist es untergegangen. Gegen 2024.tsv08kulmbach.de geprüft: dort hieß es input_alter_kind, war type="date", OPTIONAL und trug das Label „Geburtsdatum des Kindes" — genau so wieder eingebaut. Die Abteilung braucht es, weil die Gruppen nach Alter eingeteilt sind. Nur in der Turn-Variante. In der allgemeinen wäre es sinnlos und würde ohne Not personenbezogene Daten Minderjähriger erheben. Dafür drei Stellen: - contact_variant(): Schalter ask_birthdate (true nur für turnen) - contact-form.php: Feld definiert, bedingt gerendert, direkt nach der Gruppenauswahl wie auf der alten Seite - contact-submit.php: Validierung UND Rücksetzen auf leer, wenn die Variante das Feld nicht anbietet — sonst könnte ein manipulierter POST es an der allgemeinen Variante vorbei einschmuggeln Validierung serverseitig, nicht nur per type=date: echtes Datum, nicht in der Zukunft, nicht vor 1900. createFromFormat mit Rückvergleich, weil PHP sonst Scheindaten wie 2019-02-31 stillschweigend auf den 3. März verschiebt. Geprüft mit ausreichend altem Token (die Time-Trap verlangt 3 s, meine ersten Versuche liefen deshalb als Bot durch): 2019-02-31 abgewiesen, 2030-01-01 abgewiesen, gültiges Datum passiert. Ohne JS bleibt der Wert bei HTTP 422 erhalten (value="2019-03-12" im Re-Render nachgewiesen). Kein neues Markup: form-field kann type=date und min/max längst, die Prop für den Hilfetext heißt hint (nicht help, das war mein erster Fehlgriff). Docblock der Komponente um date und min/max ergänzt. Datenschutzerklärung mitgezogen, wie die Projektregel es für neue Formularfelder verlangt — mit Zweckbindung (Zuordnung zur Altersgruppe) und dem ausdrücklichen Hinweis, dass es keine Pflichtangabe ist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:10:47 +02:00
* autocomplete, maxlength, rows (textarea), min/max (date)
* options[] + placeholder (select)
* hint optionaler Hilfetext (aria-describedby)
* label_html NUR für vertrauenswürdiges Markup aus Komponenten-Code
* (z. B. Datenschutz-Link im Checkbox-Label) nie Nutzereingaben!
*
* Jedes Feld bekommt ein Fehler-Element (#<id>-error, aria-describedby): serverseitig
* gefüllt beim 422-Re-Render (form_field_error()), sonst von form.js mit der
* Browser-Validierungsmeldung. Eingaben aus einem abgewiesenen POST schreibt
* form_old() zurück deshalb gehen Werte auch ohne JS nie verloren.
*/
$type = $field['type'] ?? 'text';
$name = $field['name'];
$id = $field['id'] ?? 'field-' . $name;
$required = !empty($field['required']);
$errorId = $id . '-error';
$hintId = $id . '-hint';
$describedBy = (!empty($field['hint']) ? $hintId . ' ' : '') . $errorId;
$old = form_old($name);
$fieldError = form_field_error($id);
2026-06-20 22:18:34 +02:00
// Sichtbarer roter Stern (aria-hidden) + pro Feld erklärendes Pflicht-Wort für
// Screenreader — ersetzt die frühere globale „Pflichtfelder mit *"-Hinweiszeile.
$requiredMark = $required
2026-06-20 22:18:34 +02:00
? ' <span class="form__required" aria-hidden="true">*</span><span class="visually-hidden"> (Pflichtfeld)</span>'
: '';
$commonAttrs = 'name="' . e($name) . '" id="' . e($id) . '"'
. ($required ? ' required' : '')
. ' aria-describedby="' . e($describedBy) . '"'
. (!empty($field['autocomplete']) ? ' autocomplete="' . e($field['autocomplete']) . '"' : '')
. (!empty($field['maxlength']) ? ' maxlength="' . e($field['maxlength']) . '"' : '')
. (isset($field['min']) ? ' min="' . e((string) $field['min']) . '"' : '')
. (isset($field['max']) ? ' max="' . e((string) $field['max']) . '"' : '')
. ($fieldError !== '' ? ' aria-invalid="true"' : '');
?>
<?php if ($type === 'checkbox'): ?>
<div class="form__field form__field--checkbox">
<input type="checkbox" <?= $commonAttrs ?> value="1"<?= $old === '1' ? ' checked' : '' ?>>
<label for="<?= e($id) ?>"><?= $field['label_html'] ?? e($field['label']) ?><?= $requiredMark ?></label>
<p class="form__error-msg" id="<?= e($errorId) ?>"<?= $fieldError === '' ? ' hidden' : '' ?>><?= e($fieldError) ?></p>
</div>
<?php else: ?>
<div class="form__field">
<label for="<?= e($id) ?>"><?= e($field['label']) ?><?= $requiredMark ?></label>
<?php if ($type === 'select'): ?>
<select <?= $commonAttrs ?>>
<option value=""><?= e($field['placeholder'] ?? 'Bitte wählen') ?></option>
<?php foreach ($field['options'] as $option): ?>
<option value="<?= e($option) ?>"<?= $old !== '' && $old === (string) $option ? ' selected' : '' ?>><?= e($option) ?></option>
<?php endforeach; ?>
</select>
<?php elseif ($type === 'textarea'): ?>
<textarea <?= $commonAttrs ?> rows="<?= e($field['rows'] ?? 6) ?>" placeholder=" "><?= e($old) ?></textarea>
<?php else: ?>
<input type="<?= e($type) ?>" <?= $commonAttrs ?> value="<?= e($old) ?>" placeholder=" ">
<?php endif; ?>
<?php if (!empty($field['hint'])): ?>
<p class="form__hint" id="<?= e($hintId) ?>"><?= e($field['hint']) ?></p>
<?php endif; ?>
<p class="form__error-msg" id="<?= e($errorId) ?>"<?= $fieldError === '' ? ' hidden' : '' ?>><?= e($fieldError) ?></p>
</div>
<?php endif; ?>