Files
KantSites/templates/admin/forms.html.twig
T
kantarciandClaude Opus 5 711af312af Pick images, pages, forms, sections and terms instead of typing ids and addresses
The share image and featured image of a page become image fields with the library dialog,
as do image settings of plugins. The editor offers forms, sections, categories and tags as
lists, a page search next to every link target and in text links, the redirect target has a
page search, and the form editor picks the reply field, the condition and the option images.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 09:35:05 +02:00

243 lines
17 KiB
Twig
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.
{% extends 'admin/shell.html.twig' %}
{% set nav = 'forms' %}
{# Die Vorschau zeichnet den Absendeknopf mit dem Standardbutton der Website - dafuer braucht die
Seite dessen Stylesheet, genau wie die Designseite. #}
{% block styles %}<link rel="stylesheet" href="/assets/buttons.css?v={{ kant_version() }}"><link rel="stylesheet" href="/assets/color-control.css?v={{ kant_version() }}"><link rel="stylesheet" href="/assets/media-dialog.css?v={{ kant_version() }}">{% endblock %}
{% block scripts %}<script type="module" src="/assets/form-editor.js?v={{ kant_version() }}"></script>{% endblock %}
{% block title %}Formulare{% endblock %}
{% block heading %}Formulare{% endblock %}
{% block lead %}Felder, Empfänger, Einwilligungstext und Aussehen werden hier gepflegt. Auf einer Seite steht nur ein Formularblock mit der Adresse – so kann ein gespeicherter Seiteninhalt niemals ändern, wohin eine Anfrage geht.{% endblock %}
{% block actions %}<a class="primary" href="#neues-formular">Neues Formular</a>{% endblock %}
{# Eine Feldkarte. Der Index steht in den Feldnamen; die Vorlage fuer neue Felder traegt an seiner
Stelle den Platzhalter __n__, den das Skript beim Einfuegen ersetzt. #}
{% macro field(f, n, row, types) %}
{% set id = 'f' ~ f ~ '-' ~ n %}
<li class="field-card{{ row.label == '' ? ' field-card-new' : '' }}" data-field>
<div class="field-card-head">
<span class="field-card-index" aria-hidden="true" data-field-index></span>
<div class="field-card-title">
<label for="{{ id }}-label">Beschriftung</label>
<input id="{{ id }}-label" name="fields[{{ n }}][label]" maxlength="120" value="{{ row.label }}" placeholder="{{ row.label == '' ? 'Neues Feld – Beschriftung eingeben' : '' }}" data-field-label>
</div>
<div class="field-card-type">
<label for="{{ id }}-type">Typ</label>
<select id="{{ id }}-type" name="fields[{{ n }}][type]" data-field-type>
{% for value, label in types %}<option value="{{ value }}"{{ row.type == value ? ' selected' : '' }}>{{ label }}</option>{% endfor %}
</select>
</div>
<button type="button" class="field-remove" data-remove-field hidden aria-label="Feld entfernen" title="Feld entfernen">{% include 'admin/icon.html.twig' with {name: 'remove'} only %}</button>
</div>
<div class="field-card-body">
<div>
<label for="{{ id }}-name">Technischer Name</label>
<input id="{{ id }}-name" name="fields[{{ n }}][name]" maxlength="32" value="{{ row.name }}" pattern="[a-z][a-z0-9_]*" placeholder="vorname" data-field-name>
<p class="field-help">Kleinbuchstaben, Ziffern, Unterstrich. Steht in der E-Mail vor der Antwort.</p>
</div>
<div>
<label for="{{ id }}-help">Hilfetext</label>
<input id="{{ id }}-help" name="fields[{{ n }}][help]" maxlength="200" value="{{ row.help }}" placeholder="Steht unter dem Feld" data-field-help>
</div>
<div data-field-options>
<label for="{{ id }}-options">Optionen der Auswahl</label>
<input id="{{ id }}-options" name="fields[{{ n }}][options]" maxlength="500" value="{{ row.options|default([])|join(', ') }}" placeholder="Beratung, Angebot, Sonstiges" data-field-options-input>
<p class="field-help">Durch Komma trennen, mindestens zwei.</p>
<div class="field-card-images" data-field-images>
<label for="{{ id }}-images">Bild je Option</label>
<textarea id="{{ id }}-images" name="fields[{{ n }}][images]" rows="2" data-field-images-input>{% for option, kennung in row.images|default({}) %}{{ option }} = {{ kennung }}
{% endfor %}</textarea>
<p class="field-help">Eine Zeile je Option: <code>Option = Kennung aus der Mediathek</code>. Sobald eine Option ein Bild hat, wird die Auswahl auf der Website zur Bildauswahl – anklickbare Karten statt Kästchen.</p>
</div>
</div>
<div class="field-card-flags">
<label class="check-label" for="{{ id }}-required"><input id="{{ id }}-required" type="checkbox" name="fields[{{ n }}][required]" value="1"{{ row.required ? ' checked' : '' }} data-field-required> Pflichtfeld</label>
</div>
<div class="field-card-number" data-field-number>
<div>
<label for="{{ id }}-min">Untergrenze</label>
<input id="{{ id }}-min" name="fields[{{ n }}][min]" inputmode="decimal" value="{{ row.min|default('') }}" placeholder="0">
</div>
<div>
<label for="{{ id }}-max">Obergrenze</label>
<input id="{{ id }}-max" name="fields[{{ n }}][max]" inputmode="decimal" value="{{ row.max|default('') }}" placeholder="1000">
</div>
<div>
<label for="{{ id }}-unit">Einheit</label>
<input id="{{ id }}-unit" name="fields[{{ n }}][unit]" maxlength="12" value="{{ row.unit|default('') }}" placeholder="m²" data-field-unit>
</div>
</div>
<div class="field-card-condition" data-field-condition>
<div>
<label for="{{ id }}-showif">Nur zeigen, wenn das Feld</label>
<input id="{{ id }}-showif" name="fields[{{ n }}][showIfField]" maxlength="32" value="{{ row.showIf.field|default('') }}" pattern="[a-z][a-z0-9_]*" placeholder="immobilienart" data-field-showif>
</div>
<div>
<label for="{{ id }}-showvalues">… einen dieser Werte hat</label>
<input id="{{ id }}-showvalues" name="fields[{{ n }}][showIfValues]" maxlength="500" value="{{ row.showIf.values|default([])|join(', ') }}" placeholder="Wohnung, Haus" data-field-showvalues>
</div>
<p class="field-help">Technischer Name eines Auswahlfeldes weiter oben, Werte durch Komma. Bei einem Schritt gilt die Bedingung für alle seine Felder. Leer: das Feld gilt immer.</p>
</div>
</div>
</li>
{% endmacro %}
{% block content %}
{% import _self as karte %}
<div class="form-screens" data-form-schema="{{ schema|json_encode|e('html_attr') }}" data-form-images="{{ images|json_encode|e('html_attr') }}">
{% for form in forms|merge([null]) %}
{% set new = form is null %}
{% set f = loop.index %}
{% set rows = new ? [] : form.fields %}
{% set settings = new ? {} : form.settings %}
{% if new %}
<details class="card form-new" id="neues-formular"{{ forms is empty ? ' open' : '' }}>
<summary>Neues Formular anlegen</summary>
{% else %}
<section class="card">
{% endif %}
<form action="{{ path('admin_form_save', {slug: new ? 'neu' : form.slug}) }}" method="post" class="form-def" data-form-editor>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('forms') }}">
<input type="hidden" name="expectedVersion" value="{{ new ? 0 : form.version }}">
{% if new %}
{# Die Zusammenfassung des Details ist keine Ueberschrift; ohne diese springt die
Gliederung von der Seitenueberschrift direkt zu den Abschnittstiteln. #}
<h2 class="visually-hidden">Neues Formular</h2>
{% else %}
<div class="card-head">
<h2>{{ form.name }} <span class="count">{{ form.fields|length }} {{ form.fields|length == 1 ? 'Feld' : 'Felder' }}</span></h2>
<span class="muted">Im Editor über den Formularblock wählbar · Adresse <code>{{ form.slug }}</code></span>
</div>
{% endif %}
<div class="card-body form-editor">
<div class="form-editor-main">
<h3 class="form-part">Zustellung</h3>
<div class="fields">
<div>
<label for="name-{{ f }}">Name</label>
<input id="name-{{ f }}" name="name" maxlength="120" required value="{{ new ? '' : form.name }}" placeholder="Kontakt">
</div>
{% if new %}
<div>
<label for="slug-new">Adresse</label>
<input id="slug-new" name="slug" maxlength="80" pattern="[a-z0-9]+(-[a-z0-9]+)*" required placeholder="kontakt">
<p class="field-help">Kurzname des Formulars, etwa kontakt. Kleinbuchstaben, Ziffern, Bindestrich. Im Editor wird das Formular dann aus einer Liste gewählt.</p>
</div>
{% endif %}
<div>
<label for="recipient-{{ f }}">Empfänger</label>
<input id="recipient-{{ f }}" name="recipient" type="email" required value="{{ settings.recipient|default('') }}" placeholder="info@beispiel.de">
</div>
<div>
<label for="sender-{{ f }}">Absender</label>
<input id="sender-{{ f }}" name="sender" type="email" value="{{ settings.sender|default('') }}" placeholder="formular@beispiel.de">
<p class="field-help">Leer: die Empfängeradresse. Viele Anbieter stellen nur zu, was von einer Adresse der eigenen Domain kommt.</p>
</div>
<div>
<label for="subject-{{ f }}">Betreff der E-Mail</label>
<input id="subject-{{ f }}" name="subject" maxlength="160" value="{{ settings.subject|default('') }}" placeholder="Neue Formularnachricht">
</div>
<div>
<label for="reply-{{ f }}">Antwortadresse aus Feld</label>
<input id="reply-{{ f }}" name="replyToField" maxlength="32" value="{{ settings.replyToField|default('') }}" placeholder="email">
<p class="field-help">Technischer Name eines E-Mail-Feldes. Dann lässt sich direkt antworten.</p>
</div>
<div class="field-wide">
<label for="success-{{ f }}">Meldung nach dem Absenden</label>
<textarea id="success-{{ f }}" name="success" maxlength="300" rows="2" placeholder="Vielen Dank. Wir melden uns in Kürze bei dir.">{{ settings.success|default('') }}</textarea>
</div>
<div class="field-wide">
<label class="check-label" for="summary-{{ f }}"><input id="summary-{{ f }}" type="checkbox" name="summary" value="1"{{ settings.summary|default(false) ? ' checked' : '' }}> Zusammenfassung vor dem Absenden zeigen</label>
<p class="field-help">Wirkt bei Formularen mit Schritten: der letzte Schritt zeigt alle Angaben mit „Ändern“. Ohne Skript im Browser steht das Formular als Ganzes auf der Seite.</p>
</div>
</div>
<div class="form-fields-head">
<h3 class="form-part">Felder</h3>
<button type="button" class="secondary" data-add-field hidden>{% include 'admin/icon.html.twig' with {name: 'plus'} only %}Feld hinzufügen</button>
</div>
<ol class="field-list" data-field-list>
{% for row in rows %}{{ karte.field(f, loop.index0, row, types) }}{% endfor %}
{{ karte.field(f, rows|length, {name: '', type: 'text', label: '', required: false, help: '', options: []}, types) }}
</ol>
<template data-field-template>{{ karte.field(f, '__n__', {name: '', type: 'text', label: '', required: false, help: '', options: []}, types) }}</template>
<p class="field-help" data-fields-help>Ein Feld ohne Beschriftung wird nicht gespeichert – so lässt sich ein Feld auch ohne Skript entfernen. Ein Einwilligungsfeld ist immer ein Pflichtfeld; der Zeitpunkt der Einwilligung wird mitgespeichert. Ein Feld vom Typ „Neuer Schritt“ eröffnet einen Abschnitt: seine Beschriftung ist der Titel, sein Hilfetext die Einleitung; alle Felder bis zum nächsten Schritt gehören dazu. Ein Feld oder ein Schritt kann von einer früheren Auswahl abhängen – dann verlangt der Server es nur, wenn die Bedingung zutrifft.</p>
<h3 class="form-part">Aussehen</h3>
<div class="fields form-look">
<div>
<label for="buttonLabel-{{ f }}">Beschriftung des Buttons</label>
<input id="buttonLabel-{{ f }}" name="buttonLabel" maxlength="40" value="{{ settings.buttonLabel|default('') }}" placeholder="Absenden" data-form-button-label>
</div>
{% for key, label in {fieldBorderColor: 'Rahmen der Felder', fieldBackgroundColor: 'Hintergrund der Felder', buttonColor: 'Button', buttonInk: 'Buttonschrift'} %}
<div>
<label for="{{ key }}-{{ f }}">{{ label }}</label>
<input id="{{ key }}-{{ f }}" name="{{ key }}" value="{{ settings[key]|default('') }}" placeholder="Wie die Website" data-form-color="{{ key }}">
</div>
{% endfor %}
</div>
<p class="field-help">Leer heißt: wie die Website. Sonst eine Farbe aus der Palette oder ein Wert wie <code>#1a2b3c</code>. Form und Größe des Buttons kommen aus dem Design.</p>
</div>
{# Die Vorschau: dieselben Felder, dieselben Farben, der Standardbutton der Website.
Ohne Skript steht sie mit den gespeicherten Feldern da; mit Skript folgt sie jeder
Eingabe. #}
<aside class="form-editor-preview" aria-label="Vorschau des Formulars">
<h3 class="form-part">Vorschau</h3>
<div class="form-preview" data-form-preview>
{% for row in rows %}
{% if row.type == 'step' %}
<div class="form-preview-step"><span class="form-preview-step-title">{{ row.label }}</span>{% if row.help %}<span class="form-preview-help">{{ row.help }}</span>{% endif %}</div>
{% else %}
<div class="form-preview-field{{ row.showIf|default(null) ? ' form-preview-conditional' : '' }}">
{% if row.type == 'consent' %}
<span class="form-preview-check"><span class="form-preview-box" aria-hidden="true"></span>{{ row.label }} *</span>
{% elseif row.type == 'radio' or row.type == 'checkboxes' %}
<span class="form-preview-label">{{ row.label }}{% if row.required %} *{% endif %}</span>
{% for option in row.options %}<span class="form-preview-check"><span class="form-preview-box{{ row.type == 'radio' ? ' form-preview-radio' : '' }}" aria-hidden="true"></span>{{ option }}{% if row.images[option]|default('') %} <span class="muted">(mit Bild)</span>{% endif %}</span>{% endfor %}
{% else %}
<span class="form-preview-label">{{ row.label }}{% if row.required %} *{% endif %}</span>
<span class="form-preview-input{{ row.type == 'textarea' ? ' form-preview-area' : '' }}{{ row.type == 'select' ? ' form-preview-select' : '' }}{{ row.type == 'number' ? ' form-preview-number' : '' }}" aria-hidden="true">{{ row.type == 'select' ? '— bitte wählen —' : (row.type == 'number' ? row.unit|default('') : '') }}</span>
{% endif %}
{% if row.showIf|default(null) %}<span class="form-preview-help">Nur wenn {{ row.showIf.field }} = {{ row.showIf.values|join(' oder ') }}</span>{% endif %}
</div>
{% endif %}
{% endfor %}
<span class="cms-button b-desktop-shape-{{ schema.buttons.shape }} b-desktop-appearance-{{ schema.buttons.appearance }} b-desktop-size-{{ schema.buttons.size }}" data-preview-button>{{ settings.buttonLabel|default('Absenden') }}</span>
</div>
<p class="field-help">Schrift, Abstände und Buttonform kommen aus dem Design der Website.</p>
</aside>
</div>
<div class="card-foot form-editor-foot">
<button class="primary" type="submit">{{ new ? 'Formular anlegen' : 'Formular speichern' }}</button>
{% if not new %}<span class="muted">Änderungen gelten sofort auf jeder Seite, die dieses Formular zeigt.</span>{% endif %}
</div>
</form>
{% if new %}
</details>
{% else %}
</section>
{% endif %}
{% endfor %}
</div>
<div class="section-head"><h2>Einsendungen <span class="count">{{ submissions.total }}</span></h2><span class="muted">Neueste zuerst.</span></div>
{% if submissions.items is empty %}
<div class="empty"><h3>Noch keine Einsendungen.</h3><p>Hier erscheinen eingegangene Nachrichten, auch wenn der E-Mail-Versand fehlschlägt.</p></div>
{% else %}
<div class="rows">
{% for entry in submissions.items %}
<article class="row row-top">
<div class="row-main">
<h3>{{ entry.form }}</h3>
<p>{{ entry.createdAt|date('d.m.Y H:i') }} UTC{% if entry.consentAt %} · Einwilligung {{ entry.consentAt|date('d.m.Y H:i') }}{% endif %}</p>
<dl class="submission-data">{% for key, value in entry.data %}<dt>{{ key }}</dt><dd>{{ value }}</dd>{% endfor %}</dl>
</div>
<span class="pill {{ entry.delivered ? 'pill-live' : 'pill-off' }}">{{ entry.delivered ? 'Versendet' : 'Nicht versendet' }}</span>
</article>
{% endfor %}
</div>
{% endif %}
{% include 'admin/pager.html.twig' with {result: submissions, route: 'admin_forms', label: 'Seiten der Einsendungen'} only %}
<p class="footnote">KantSites · Arbeitsstand</p>
{% endblock %}