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

+121 -15
View File
@@ -9,6 +9,7 @@
*/
import { createColorControl } from './color-control.js';
import { buttonSample } from './button-design.js';
import { openMedia } from './media.js';
const root = document.querySelector('[data-form-schema]');
if (root) {
@@ -115,8 +116,10 @@ if (root) {
const note = el('p', 'contrast-note'); note.hidden = true; note.setAttribute('role', 'status');
preview.after(note);
// Statt Kennungen zu tippen: je Option eine Liste der Mediathek. Das Textfeld bleibt das
// gespeicherte Original und wird aus den Listen geschrieben; ohne Skript tippt man hinein.
// Statt Kennungen zu tippen: je Option ein Bild aus der Mediathek, gewählt im Dialog mit
// Vorschau und Hochladen. Das Textfeld bleibt das gespeicherte Original und wird aus der
// Auswahl geschrieben; ohne Skript tippt man hinein.
const bildVon = new Map(bilder.map(bild => [bild.id, bild]));
const renderImagePickers = card => {
const area = card.querySelector('[data-field-images-input]');
const options = card.querySelector('[data-field-options-input]').value.split(',').map(o => o.trim()).filter(Boolean);
@@ -129,23 +132,124 @@ if (root) {
const current = Object.fromEntries(area.value.split('\n').filter(l => l.includes('=')).map(l => l.split('=').map(x => x.trim())));
box.dataset.signature = options.join('|');
box.replaceChildren();
const write = () => {
const map = {};
for (const row of box.querySelectorAll('[data-option]')) if (row.dataset.media) map[row.dataset.option] = row.dataset.media;
area.value = Object.entries(map).map(([o, id]) => `${o} = ${id}`).join('\n');
renderPreview();
};
for (const option of options) {
const row = el('label', 'field-image-picker');
row.append(el('span', 'field-image-picker-option', option));
const select = document.createElement('select');
select.append(new Option('Kein Bild', ''));
for (const bild of bilder) select.append(new Option(bild.title, bild.id, false, current[option] === bild.id));
select.dataset.option = option;
select.addEventListener('change', () => {
const map = {};
for (const picker of box.querySelectorAll('select')) if (picker.value) map[picker.dataset.option] = picker.value;
area.value = Object.entries(map).map(([o, id]) => `${o} = ${id}`).join('\n');
renderPreview();
});
row.append(select); box.append(row);
const row = el('div', 'field-image-picker'); row.dataset.option = option;
const thumb = document.createElement('img'); thumb.className = 'field-image-picker-thumb'; thumb.alt = ''; thumb.width = 64; thumb.height = 48;
const name = el('span', 'field-image-picker-name');
const choose = el('button', 'secondary', ''); choose.type = 'button';
const remove = el('button', 'text-button danger-text', 'Entfernen'); remove.type = 'button';
remove.setAttribute('aria-label', `Bild für „${option}“ entfernen`);
const show = id => {
row.dataset.media = id || '';
const bild = id ? bildVon.get(id) : null;
thumb.hidden = !bild; if (bild) thumb.src = bild.src; else thumb.removeAttribute('src');
name.textContent = id ? (bild?.title || 'Bild gewählt') : 'Kein Bild';
choose.textContent = id ? 'Anderes Bild' : 'Bild wählen';
choose.setAttribute('aria-label', `${id ? 'Anderes Bild' : 'Bild'} für „${option}“ wählen`);
remove.hidden = !id;
};
choose.addEventListener('click', () => openMedia({ needsAlt: false, onChoose: ([item]) => {
if (!item) return;
bildVon.set(item.id, { id: item.id, title: item.title, src: item.variants[0].src });
show(item.id); write();
} }));
remove.addEventListener('click', () => { show(''); write(); choose.focus(); });
show(current[option] || '');
row.append(el('span', 'field-image-picker-option', option), thumb, name, choose, remove); box.append(row);
}
};
/*
* Auswahl statt Feldnamen: die Bedingung eines Feldes nennt ein Auswahlfeld weiter oben und
* dessen Optionen, die Antwortadresse ein E-Mail-Feld. Die Textfelder bleiben das
* gespeicherte Original, versteckt, und werden aus der Auswahl geschrieben.
*/
const fieldsBefore = card => {
const list = [];
for (const other of cards()) {
if (other === card) break;
const name = other.querySelector('[data-field-name]').value.trim();
const type = other.querySelector('[data-field-type]').value;
if (name && ['select', 'radio', 'checkboxes'].includes(type)) {
list.push({ name, label: other.querySelector('[data-field-label]').value.trim() || name,
options: other.querySelector('[data-field-options-input]').value.split(',').map(o => o.trim()).filter(Boolean) });
}
}
return list;
};
const renderCondition = card => {
const fieldInput = card.querySelector('[data-field-showif]');
const valuesInput = card.querySelector('[data-field-showvalues]');
const candidates = fieldsBefore(card);
const signature = JSON.stringify([candidates, fieldInput.value, valuesInput.value]);
let box = card.querySelector('[data-condition-picker]');
if (!box) {
box = el('div', 'field-condition-picker'); box.dataset.conditionPicker = '';
fieldInput.closest('div').hidden = true; valuesInput.closest('div').hidden = true;
fieldInput.closest('[data-field-condition]').prepend(box);
const help = box.parentElement.querySelector(':scope > .field-help');
if (help) help.textContent = 'Zur Wahl stehen Auswahlfelder weiter oben. Bei einem Schritt gilt die Bedingung für alle seine Felder.';
}
if (box.dataset.signature === signature) return;
box.dataset.signature = signature;
const focused = box.contains(document.activeElement) ? document.activeElement.dataset.key : null;
box.replaceChildren();
const id = `${fieldInput.id}-select`;
const label = el('label', '', 'Nur zeigen, wenn'); label.htmlFor = id;
const select = document.createElement('select'); select.id = id; select.dataset.key = 'field';
select.append(new Option('Immer zeigen', ''));
for (const candidate of candidates) select.append(new Option(candidate.label, candidate.name));
if (fieldInput.value && !candidates.some(c => c.name === fieldInput.value)) select.append(new Option(`${fieldInput.value} (nicht mehr weiter oben)`, fieldInput.value));
select.value = fieldInput.value;
select.addEventListener('change', () => { fieldInput.value = select.value; valuesInput.value = ''; refresh(); });
box.append(label, select);
const chosen = candidates.find(c => c.name === fieldInput.value);
if (chosen) {
const set = document.createElement('fieldset'); set.className = 'field-condition-values';
set.append(el('legend', '', '… eine dieser Antworten gewählt ist'));
const current = valuesInput.value.split(',').map(v => v.trim()).filter(Boolean);
chosen.options.forEach((option, index) => {
const check = el('label', 'check-label');
const box2 = document.createElement('input'); box2.type = 'checkbox'; box2.value = option; box2.checked = current.includes(option);
box2.dataset.key = `value-${index}`;
box2.addEventListener('change', () => {
valuesInput.value = [...set.querySelectorAll('input:checked')].map(input => input.value).join(', ');
refresh();
});
check.append(box2, document.createTextNode(` ${option}`)); set.append(check);
});
box.append(set);
}
if (focused) box.querySelector(`[data-key="${focused}"]`)?.focus();
};
const reply = form.querySelector('[name="replyToField"]');
const renderReply = () => {
if (!reply) return;
const emails = cards().map(card => ({ name: card.querySelector('[data-field-name]').value.trim(), label: card.querySelector('[data-field-label]').value.trim(), type: card.querySelector('[data-field-type]').value }))
.filter(field => field.name && field.type === 'email');
let select = form.querySelector('[data-reply-picker]');
if (!select) {
select = document.createElement('select'); select.dataset.replyPicker = ''; select.id = reply.id;
reply.id += '-value'; reply.type = 'hidden'; reply.after(select);
select.addEventListener('change', () => { reply.value = select.value; });
}
const signature = JSON.stringify([emails, reply.value]);
if (select.dataset.signature === signature) return;
select.dataset.signature = signature;
select.replaceChildren(new Option('Keine – Antworten gehen an den Absender der E-Mail', ''));
for (const field of emails) select.append(new Option(field.label || field.name, field.name));
if (reply.value && !emails.some(field => field.name === reply.value)) select.append(new Option(`${reply.value} (kein E-Mail-Feld mehr)`, reply.value));
select.value = reply.value;
const help = reply.parentElement.querySelector('.field-help');
if (help) help.textContent = 'Ein E-Mail-Feld des Formulars. Dann lässt sich auf die Nachricht direkt antworten.';
};
const refresh = () => {
cards().forEach((card, index) => {
card.querySelector('[data-field-index]').textContent = String(index + 1);
@@ -153,6 +257,7 @@ if (root) {
card.querySelector('[data-field-options]').hidden = !['select', 'radio', 'checkboxes'].includes(type);
card.querySelector('[data-field-images]').hidden = !['radio', 'checkboxes'].includes(type);
if (['radio', 'checkboxes'].includes(type)) renderImagePickers(card);
renderCondition(card);
card.querySelector('[data-field-number]').hidden = type !== 'number';
const remove = card.querySelector('[data-remove-field]');
remove.hidden = false; remove.setAttribute('aria-label', `Feld ${index + 1} entfernen`);
@@ -162,6 +267,7 @@ if (root) {
else if (type === 'step') { required.checked = false; required.disabled = true; }
else required.disabled = false;
});
renderReply();
renderPreview();
};