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>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-22 09:35:05 +02:00
1 parent 31fb53c1fc
commit 711af312af
19 files changed
+446 -43

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
{% set selected = selected|default(null) %}
<div class="content-picker" data-source="{{ path('admin_content_choices') }}" data-type="{{ type|default('') }}" data-parents="{{ parents|default(false) ? '1' : '0' }}"{% if valueKey|default('id') != 'id' %} data-value="{{ valueKey }}"{% endif %}>
<div class="content-picker" data-source="{{ path('admin_content_choices') }}" data-type="{{ type|default('') }}" data-parents="{{ parents|default(false) ? '1' : '0' }}"{% if valueKey|default('id') != 'id' %} data-value="{{ valueKey }}"{% endif %}{% if free|default(false) %} data-free="1"{% endif %}>
<label for="{{ id }}">{{ label|default('Seite suchen') }}</label>
<div class="picker-input">
<input id="{{ id }}" type="search" autocomplete="off" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="{{ id }}-results" placeholder="Titel oder Adresse suchen …" value="{{ selected ? selected.title ~ ' (/' ~ selected.path ~ ')' : '' }}">
+2 -4
View File
@@ -53,8 +53,7 @@
<p class="field-help">Nicht indexierte Seiten erscheinen auch nicht in der Sitemap.</p>
</div>
<div>
<label for="og-{{ page.id }}">Vorschaubild beim Teilen</label>
<input id="og-{{ page.id }}" name="ogMediaId" maxlength="32" value="{{ page.og_media_id }}" placeholder="ID aus der Mediathek">
{% include 'admin/media-field.html.twig' with {id: 'og-' ~ page.id, name: 'ogMediaId', label: 'Vorschaubild beim Teilen', value: page.og_media_id|default(''), help: 'Erscheint, wenn jemand die Seite in sozialen Netzen oder Messengern teilt.'} only %}
</div>
{% if page.type == 'post' %}
<div>
@@ -75,8 +74,7 @@
<p class="field-help">Mehrere durch Komma trennen. Unbekannte Namen werden angelegt.</p>
</div>
<div>
<label for="media-{{ page.id }}">Beitragsbild</label>
<input id="media-{{ page.id }}" name="featuredMediaId" maxlength="32" value="{{ page.featured_media_id }}" placeholder="ID aus der Mediathek">
{% include 'admin/media-field.html.twig' with {id: 'media-' ~ page.id, name: 'featuredMediaId', label: 'Beitragsbild', value: page.featured_media_id|default(''), help: 'Erscheint in Beitragslisten und im Feed.'} only %}
</div>
<div>
<label for="excerpt-{{ page.id }}">Auszug</label>
+3 -3
View File
@@ -2,7 +2,7 @@
{% 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() }}">{% endblock %}
{% 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 %}
@@ -105,7 +105,7 @@
{% 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 als Formularblock mit der Adresse <code>{{ form.slug }}</code></span>
<span class="muted">Im Editor über den Formularblock wählbar · Adresse <code>{{ form.slug }}</code></span>
</div>
{% endif %}
<div class="card-body form-editor">
@@ -120,7 +120,7 @@
<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">Wird im Formularblock eingetragen. Kleinbuchstaben, Ziffern, Bindestrich.</p>
<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>
+17
View File
@@ -0,0 +1,17 @@
{# Ein Bild aus der Mediathek als Formularfeld: Vorschau, Wählen oder Hochladen im Dialog,
Entfernen. Die Kennung steht nur im versteckten Feld - niemand soll sie abtippen müssen.
Erwartet: id, name, label, value, help (optional). #}
{% set item = value ? kant_media(value) : null %}
<div class="media-field" data-media-field role="group" aria-labelledby="{{ id }}-label">
<span class="media-field-label" id="{{ id }}-label">{{ label }}</span>
<input type="hidden" id="{{ id }}" name="{{ name }}" value="{{ value }}">
<div class="media-field-current">
<img class="media-field-thumb" alt="" width="96" height="64"{% if item %} src="{{ (item.variants|first).src }}"{% else %} hidden{% endif %}>
<span class="media-field-name" aria-live="polite">{{ item ? item.title : (value ? 'Bild nicht gefunden' : 'Kein Bild gewählt') }}</span>
</div>
<div class="media-field-actions">
<button type="button" class="secondary" data-media-choose>{{ value ? 'Anderes Bild wählen' : 'Bild wählen oder hochladen' }}</button>
<button type="button" class="text-button danger-text" data-media-remove{{ value ? '' : ' hidden' }}>Entfernen</button>
</div>
{% if help|default('') %}<p class="field-help">{{ help }}</p>{% endif %}
</div>
+2 -1
View File
@@ -4,7 +4,8 @@
{% set singular = contentType == 'post' ? 'Beitrag' : 'Seite' %}
{% block title %}{{ plural }}{% endblock %}
{% block heading %}{{ plural }}{% endblock %}
{% block scripts %}<script type="module" src="/assets/live-search.js?v={{ kant_version() }}"></script>{% endblock %}
{% block styles %}<link rel="stylesheet" href="/assets/media-dialog.css?v={{ kant_version() }}">{% endblock %}
{% block scripts %}<script type="module" src="/assets/live-search.js?v={{ kant_version() }}"></script><script type="module" src="/assets/media-field.js?v={{ kant_version() }}"></script>{% endblock %}
{% block lead %}{{ plural }} suchen, bearbeiten und veröffentlichen. Entwürfe bleiben bis zur Veröffentlichung privat.{% endblock %}
{% block content %}
+5 -2
View File
@@ -2,13 +2,17 @@
{% set nav = 'plugins' %}
{% block title %}Plugins{% endblock %}
{% block heading %}Plugins{% endblock %}
{% block styles %}<link rel="stylesheet" href="/assets/media-dialog.css?v={{ kant_version() }}">{% endblock %}
{% block scripts %}<script type="module" src="/assets/media-field.js?v={{ kant_version() }}"></script>{% endblock %}
{% block lead %}Erweiterungen bringen eigene Bausteine, Vorlagen und Adressen mit. Sie liegen als Ordner unter <code>plugins/</code>; hier schaltest du sie ein und aus und stellst sie ein. Ein ausgeschaltetes Plugin lässt seine Bausteine in den Seiten stehen, zeigt sie aber nicht mehr.{% endblock %}
{# Ein Feld aus dem Schema eines Plugins - dieselben sieben Arten wie im Editor. #}
{% macro feld(plugin, key, schema, value) %}
{% set id = 'plugin-' ~ plugin ~ '-' ~ key %}
{% set name = 'settings[' ~ key ~ ']' %}
{% if schema.kind == 'boolean' %}
{% if schema.kind == 'media' %}
<div>{% include 'admin/media-field.html.twig' with {id: id, name: name, label: schema.label, value: value|default(''), help: schema.help|default('')} only %}</div>
{% elseif schema.kind == 'boolean' %}
<div><label class="check-label" for="{{ id }}"><input id="{{ id }}" type="checkbox" name="{{ name }}" value="1"{{ value ? ' checked' : '' }}> {{ schema.label }}</label>{% if schema.help %}<p class="field-help">{{ schema.help }}</p>{% endif %}</div>
{% else %}
<div>
@@ -16,7 +20,6 @@
{% if schema.kind == 'textarea' %}<textarea id="{{ id }}" name="{{ name }}" rows="3" maxlength="{{ schema.maxLength }}">{{ value }}</textarea>
{% elseif schema.kind == 'select' %}<select id="{{ id }}" name="{{ name }}">{% for wert, text in schema.choices %}<option value="{{ wert }}"{{ value == wert ? ' selected' : '' }}>{{ text }}</option>{% endfor %}</select>
{% elseif schema.kind == 'number' %}<input id="{{ id }}" name="{{ name }}" type="number" step="any" min="{{ schema.min }}" max="{{ schema.max }}" value="{{ value }}">
{% elseif schema.kind == 'media' %}<input id="{{ id }}" name="{{ name }}" value="{{ value }}" placeholder="Kennung aus der Mediathek" pattern="[a-f0-9]{32}">
{% else %}<input id="{{ id }}" name="{{ name }}" value="{{ value }}" maxlength="{{ schema.maxLength }}">{% endif %}
{% if schema.help %}<p class="field-help">{{ schema.help }}</p>{% endif %}
</div>
+2 -3
View File
@@ -292,9 +292,8 @@
<input id="from" name="from" maxlength="400" required placeholder="alte-seite">
</div>
<div>
<label for="to">Neue Adresse</label>
<input id="to" name="to" maxlength="400" placeholder="leistungen/webdesign">
<p class="field-help">Bei 410 leer lassen.</p>
{% include 'admin/content-picker.html.twig' with {id: 'to', name: 'to', label: 'Neue Adresse', valueKey: 'path', free: true} only %}
<p class="field-help">Seite suchen und wählen – oder einen anderen Pfad eintippen, etwa kategorie/projekte. Bei 410 leer lassen.</p>
</div>
<div>
<label for="status">Art</label>