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

+31 -9
View File
@@ -1,5 +1,6 @@
import { EditorHistory, labels, blockLabel, blockIcon, isPluginBlock, setStyle, effectiveStyle, locate, clone, isContainer, canContain, insertBlock, insertBlockAt, insertionTarget, moveBlockTo, duplicateBlock, moveBlock, devices, effectiveDesign, setDesign, blockPath, insertPreset, insertRow, insertRowAt, setEffect, insertMedia, upgradeDocument, CATEGORIES, ROW_COLUMNS, whyNot, dropList } from './editor-model.js';
import { openMedia, openDocuments, createMediaPreview } from './media.js';
import { openPageDialog } from './page-picker.js';
import { createDesignPanel } from './design-panel.js';
import { renderButtonControls, setButtonDesign } from './button-design.js';
import { createRichText } from './rich-text.js';
@@ -330,13 +331,19 @@ function renderContent(block, panel) {
input.addEventListener('blur', () => { history.mergeKey = null; });
if (options.checkbox) container.append(labelEl); else container.append(labelEl, input);
if (options.help) { const help = document.createElement('p'); help.className = 'field-help'; help.id = `${id}-help`; help.textContent = options.help; input.setAttribute('aria-describedby', help.id); container.append(help); }
if (key === 'href') {
if (key === 'href' || options.link) {
// Ein PDF ist ein Linkziel wie jedes andere: seine Adresse steht danach im Feld. Ohne neuen
// Tab zeigt die Website es im Betrachter über der Seite.
// Eine eigene Seite sucht man, statt ihren Pfad zu kennen.
const choose = value => { input.value = value; applyValue(); history.mergeKey = null; };
const pages = document.createElement('button'); pages.type = 'button'; pages.className = 'secondary href-document'; pages.textContent = 'Seite wählen';
pages.addEventListener('click', () => openPageDialog({ onChoose: item => choose(item.path === 'home' ? '/' : `/${item.path}`) }));
const pdf = document.createElement('button'); pdf.type = 'button'; pdf.className = 'secondary href-document'; pdf.textContent = 'PDF aus Mediathek wählen';
pdf.addEventListener('click', () => openDocuments({ onChoose: item => { input.value = item.src; applyValue(); history.mergeKey = null; } }));
container.append(pdf);
field('In neuem Tab öffnen', 'newTab', block.props.newTab === true, { checkbox: true, container });
pdf.addEventListener('click', () => openDocuments({ onChoose: item => choose(item.src) }));
const row = document.createElement('div'); row.className = 'href-pickers'; row.append(pages, pdf);
container.append(row);
// Den Schalter kennen nur die Linkbausteine des Kerns; ein Plugin legt seine Felder selbst fest.
if (key === 'href' && !options.plugin) field('In neuem Tab öffnen', 'newTab', block.props.newTab === true, { checkbox: true, container });
}
};
if (!block) {
@@ -346,6 +353,21 @@ function renderContent(block, panel) {
}
const plugin = pluginDefinition(block);
if (plugin) { renderPluginFields(block, panel, plugin, field); return; }
/*
* Eine Auswahl statt einer abgetippten Adresse, für alles, was ein Baustein nur verknüpft.
* Eine gespeicherte Adresse, die es nicht mehr gibt, bleibt sichtbar statt still zu
* verschwinden. Ohne die Liste (sie kommt mit dem Design-Schema) bleibt es ein Textfeld.
*/
const pick = (kind, current, empty = null) => {
const list = designSchema?.choices?.[kind];
if (!list) return {};
const choices = {};
if (empty !== null) choices[''] = empty;
else if (!current) choices[''] = 'Bitte wählen …';
Object.assign(choices, list);
if (current && !(current in list)) choices[current] = `${current} (nicht mehr vorhanden)`;
return { choices };
};
if (block.type === 'text') {
// Formatted text with a toolbar and an HTML view. The document moves to v12 on the first
// formatted save; until then it keeps plain text semantics and stays escaped when rendered.
@@ -410,7 +432,7 @@ function renderContent(block, panel) {
field('Menü-Knopf', 'burgerLabel', block.props.burgerLabel || 'both', { choices: { both: 'Symbol und Text', text: 'Nur Text', icon: 'Nur Symbol' }, help: 'Wie der aufklappbare Knopf beschriftet ist. „Nur Symbol“ behält den Namen für Vorlesesoftware. Farben, Rahmen und Schriftgröße des Knopfes stehen unter Design → Menü-Knopf.' });
field('Automatisch als Hamburger', 'collapse', block.props.collapse || 'tablet', { choices: { tablet: 'Tablet und Mobil (Standard)', mobile: 'Nur Mobil', never: 'Nie' }, help: 'Waagerechte Menüs wechseln standardmäßig bis 1024 px und auf Touchgeräten ohne Maus, auch großen iPads. „Nur Mobil“ gilt bis 767 px. Die Menüarten „Untereinander“ und „Burger“ bleiben unverändert.' });
}
if (block.type === 'partialref') field('Adresse der Sektion', 'partial', block.props.partial, { help: 'Die Adresse aus der Bereichsverwaltung. Die Sektion wird verknüpft, nicht kopiert: eine Änderung dort wirkt auf allen Seiten, die sie einbinden.' });
if (block.type === 'partialref') field('Sektion', 'partial', block.props.partial, { ...pick('sections', block.props.partial), help: 'Die Sektion wird verknüpft, nicht kopiert: eine Änderung dort wirkt auf allen Seiten, die sie einbinden. Angelegt und bearbeitet wird sie unter „Bereiche“.' });
if (block.type === 'icon') {
field('Symbol', 'icon', block.props.icon, { choices: Object.fromEntries(ICONS.map(name => [name, name])) });
// number, so the value is stored as a number: the server wants an int, not "48".
@@ -490,7 +512,7 @@ function renderContent(block, panel) {
: 'Eigenes HTML kann nur ein Administrator ändern, weil es unverändert auf der veröffentlichten Seite landet. Der Inhalt ist hier nur zu sehen.';
panel.append(label, input, note);
}
if (block.type === 'form') field('Adresse des Formulars', 'form', block.props.form, { help: 'Die Adresse aus der Formularverwaltung, zum Beispiel kontakt. Felder, Empfänger und Einwilligungstext werden dort gepflegt, nicht hier.' });
if (block.type === 'form') field('Formular', 'form', block.props.form, { ...pick('forms', block.props.form), help: 'Felder, Empfänger und Einwilligungstext werden unter „Formulare“ gepflegt, nicht hier.' });
if (block.type === 'tabitem') field('Titel', 'title', block.props.title, { maxLength: 160, help: 'Beschriftung des Tabs. In der Vorschau sind alle Tabs untereinander sichtbar, weil die Vorschau kein JavaScript ausführt.' });
if (block.type === 'accordion') {
field('Als FAQ auszeichnen', 'faq', block.props?.faq === true, { checkbox: true, help: 'Die Einträge erscheinen als Frage-Antwort-Paare in den strukturierten Daten (FAQPage): der Titel ist die Frage, die Textbausteine im Eintrag sind die Antwort.' });
@@ -526,8 +548,8 @@ function renderContent(block, panel) {
}
if (block.type === 'postlist') {
field('Anzahl Beiträge', 'count', block.props.count, { number: true, help: 'Zwischen 1 und 24. Die Beiträge selbst kommen beim Ausliefern dazu, nicht aus dem gespeicherten Inhalt.' });
field('Nur Kategorie', 'category', block.props.category || '', { help: 'Adresse der Kategorie, zum Beispiel projekte. Leer lassen für alle Beiträge.' });
field('Nur Schlagwort', 'tag', block.props.tag || '', { help: 'Adresse des Schlagworts. Leer lassen für alle Beiträge.' });
field('Nur Kategorie', 'category', block.props.category || '', { ...pick('categories', block.props.category, 'Alle Kategorien') });
field('Nur Schlagwort', 'tag', block.props.tag || '', { ...pick('tags', block.props.tag, 'Alle Schlagwörter'), help: 'Kategorien und Schlagwörter entstehen in den Einstellungen eines Beitrags.' });
field('Darstellung', 'layout', block.props.layout || 'karten', { choices: { karten: 'Karten (Raster mit Bild oben)', liste: 'Liste (Bild links, Text rechts)', kompakt: 'Kompakt (nur Titel und Datum)', wechsel: 'Wechselseitig (Bild mal links, mal rechts)' }, help: 'Dieselben Beiträge, drei Darstellungen. Karten für eine Übersicht, Liste für ein Archiv, Kompakt für eine Randspalte.' });
field('Blättern statt abschneiden', 'paginate', block.props.paginate === true, { checkbox: true, help: 'Für eine Übersichtsseite. Eine Auswahl auf der Startseite lässt man aus.' });
field('Datum und Autor zeigen', 'showMeta', block.props.showMeta !== false, { checkbox: true });
@@ -774,7 +796,7 @@ function renderPluginFields(block, panel, plugin, field) {
if (schema.help) { const help = document.createElement('p'); help.className = 'field-help'; help.textContent = schema.help; wrap.append(help); }
panel.append(wrap);
}
else field(schema.label, key, value, { ...options, maxLength: schema.maxLength });
else field(schema.label, key, value, { ...options, maxLength: schema.maxLength, plugin: true, link: schema.kind === 'url' });
}
}