fix: expose accordion contents and add clear insert actions (1.8.3)
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s

This commit is contained in:
kantarci committed 2026-09-14 12:53:16 +02:00
1 parent a108e7d143
commit a3029966cb
12 files changed
+257 -18

No files matched your search

+2
View File
@@ -148,6 +148,8 @@ export function insertionTarget(document, selected, type) {
}
const probe = { type };
let item = locate(document.blocks, selected);
// Content belongs to a chosen entry. Do not silently place it outside the accordion.
if (item?.block.type === 'accordion' && type !== 'accordionitem') return null;
if (item && canContain(item.block, probe)) return { parentId: item.block.id, index: (item.block.children ?? []).length };
while (item) {
const parentId = item.parent?.id ?? null;
+3
View File
@@ -110,6 +110,9 @@ dialog p{font-size:14px;line-height:1.7;color:var(--muted)}
.property-tabs{display:flex;background:#edf1e7;padding:4px;border-radius:8px;gap:4px}
.property-tabs button{flex:1;min-width:0;border:0;background:transparent;color:var(--ink);border-radius:6px;padding:10px;font-size:12px;font-weight:600}
.property-tabs [aria-pressed=true]{background:white;box-shadow:0 1px 3px #0001}
.accordion-entry-list{display:grid;gap:7px;margin:12px 0}
.accordion-entry-choice{min-height:44px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:#f7f9f3;color:var(--ink);font-size:12px;line-height:1.5;text-align:left;overflow-wrap:anywhere}
.accordion-entry-choice:hover{border-color:var(--accent);background:#edf3e7}
.design-scope{font-size:12px;font-weight:600;line-height:1.6;margin:18px 0 8px;padding:10px;border-left:3px solid var(--accent);background:#f1f5e7}
.design-reset{margin-top:24px;white-space:normal;width:100%}
.breadcrumbs{display:flex;align-items:center;flex-wrap:wrap;gap:3px;min-height:35px;padding:3px 14px;background:#f6f8f2;border-top:1px solid var(--line)}
+57 -8
View File
@@ -101,7 +101,7 @@ function controls() {
function renderTree() {
const activeId = $('#structure').contains(document.activeElement) ? document.activeElement.dataset.blockId : null;
const query = $('#tree-search').value.trim().toLocaleLowerCase('de');
const matches = block => `${blockLabel(block)} ${block.props?.text || ''} ${block.props?.alt || ''}`.toLocaleLowerCase('de').includes(query) || (block.children || []).some(matches);
const matches = block => `${blockLabel(block)} ${block.props?.text || ''} ${block.props?.alt || ''} ${block.props?.title || ''}`.toLocaleLowerCase('de').includes(query) || (block.children || []).some(matches);
const build = blocks => {
const list = document.createElement('ul'); list.className = 'structure-list';
for (const block of blocks) {
@@ -119,9 +119,10 @@ function renderTree() {
button.className = 'tree-item'; button.type = 'button'; button.draggable = true;
button.dataset.blockId = block.id;
button.setAttribute('aria-pressed', String(block.id === selected));
button.setAttribute('aria-label', `${blockLabel(block)}: ${block.props?.text || block.props?.alt || blockLabel(block)}`);
const displayText = block.props?.text || block.props?.alt || block.props?.title || blockLabel(block);
button.setAttribute('aria-label', `${blockLabel(block)}: ${displayText}`);
const icon = document.createElement('span'); icon.className = 'tree-icon'; icon.setAttribute('aria-hidden', 'true'); icon.textContent = blockIcon(block);
const text = document.createElement('span'); text.textContent = block.props?.text || block.props?.alt || blockLabel(block);
const text = document.createElement('span'); text.textContent = displayText;
button.append(icon, text); item.append(button);
if (block.children?.length && (!collapsed.has(block.id) || query)) item.append(build(block.children));
list.append(item);
@@ -323,9 +324,28 @@ function renderProperties() {
}
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 === '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') {
const hint = document.createElement('p'); hint.className = 'field-help';
hint.textContent = 'Wähle einen Eintrag, um seinen Titel und Inhalt zu bearbeiten. Texte, Bilder und weitere Elemente gehören in den jeweiligen Eintrag.';
const entries = document.createElement('div'); entries.className = 'accordion-entry-list';
for (const [index, entry] of (block.children ?? []).entries()) {
const button = document.createElement('button'); button.type = 'button'; button.className = 'accordion-entry-choice';
button.textContent = `${index + 1}. ${entry.props?.title || 'Ohne Titel'}`;
button.setAttribute('aria-label', `Eintrag ${index + 1}: ${entry.props?.title || 'Ohne Titel'} bearbeiten`);
button.addEventListener('click', () => { propertyPanel = 'content'; choose(entry.id); });
entries.append(button);
}
const add = document.createElement('button'); add.type = 'button'; add.className = 'secondary full-width';
add.textContent = '+ Eintrag hinzufügen'; add.addEventListener('click', () => addAccordionEntry(block.id));
panel.append(hint, entries, add);
}
if (block.type === 'accordionitem') {
field('Titel', 'title', block.props.title, { maxLength: 160 });
field('Beim Laden geöffnet', 'open', block.props.open, { checkbox: true });
field('Beim Laden geöffnet', 'open', block.props.open, { checkbox: true, help: 'Gilt für die veröffentlichte Seite. Im Editor bleiben alle Einträge zum Bearbeiten geöffnet.' });
const add = document.createElement('button'); add.type = 'button'; add.className = 'secondary full-width';
add.textContent = '+ Inhalt hinzufügen';
add.addEventListener('click', () => beginContainerInsert(block.id));
panel.append(add);
}
if (block.type === 'legal') {
field('Beschriftung für Vorlesesoftware', 'label', block.props?.label || 'Rechtliches', { maxLength: 60, help: 'Der Baustein zeigt Impressum, Datenschutz und – sobald jemand gefragt wurde – den Weg zurück zur Einwilligung. Die Adressen stehen in den Einstellungen. Platziere ihn dort, wo die Links erscheinen sollen, zum Beispiel im Fußbereich.' });
@@ -722,14 +742,16 @@ async function updatePreview() {
const id = insert.dataset.editorTarget;
if (!locate(history.document.blocks, id)) return;
choose(id, false);
if (insert.hasAttribute('data-editor-row')) {
if (insert.hasAttribute('data-editor-add-entry')) {
addAccordionEntry(id);
} else if (insert.hasAttribute('data-editor-row')) {
setPendingInsert(null);
selected = history.change(content => insertRowAt(content, Number(insert.dataset.editorRow), id, 0));
propertyPanel = 'content'; changed();
} else if (locate(history.document.blocks, id).block.type === 'row') {
selected = history.change(content => insertBlockAt(content, 'column', id, 0));
changed();
} else setPendingInsert({ parentId: id, index: 0 });
} else beginContainerInsert(id);
return;
}
if (inlineEdit) return;
@@ -857,9 +879,28 @@ function addElement(type) {
selected = history.change(doc => at
? insertBlockAt(doc, type, at.parentId, at.index) ?? insertBlock(doc, type, selected)
: insertBlock(doc, type, selected));
if (type === 'accordion') selected = locate(history.document.blocks, selected)?.block.children?.[0]?.id ?? selected;
if (['accordion', 'accordionitem'].includes(type)) propertyPanel = 'content';
changed();
}
function beginContainerInsert(id) {
const block = locate(history.document.blocks, id)?.block;
if (!block || busy) return;
propertyPanel = 'content';
choose(id, false);
setPendingInsert({ parentId: id, index: block.children?.length ?? 0 });
}
function addAccordionEntry(id) {
const block = locate(history.document.blocks, id)?.block;
if (busy || block?.type !== 'accordion') return;
setPendingInsert(null);
selected = history.change(doc => insertBlockAt(doc, 'accordionitem', id, block.children?.length ?? 0));
collapsed.delete(id); propertyPanel = 'content'; changed();
$('#property-title')?.focus();
}
/** Marks where the next added element goes, and says so instead of leaving it invisible. */
function setPendingInsert(target) {
pendingInsert = target;
@@ -945,7 +986,11 @@ function updateLibrary() {
? (dropList(history.document, pendingInsert.parentId, { type }) ? pendingInsert : null)
: insertionTarget(history.document, selected, type);
button.disabled = !target;
if (!target) { button.title = whyNot(type); continue; }
if (!target) {
button.title = locate(history.document.blocks, selected)?.block.type === 'accordion'
? 'Wähle zuerst einen Akkordeon-Eintrag, um darin Inhalte einzufügen.' : whyNot(type);
continue;
}
const parent = target.parentId ? locate(history.document.blocks, target.parentId)?.block : null;
button.title = `Wird in ${parent ? blockLabel(parent) : 'die Seite'} eingefügt.`;
}
@@ -1092,7 +1137,11 @@ overlay = createCanvasOverlay({
const id = history.change(doc => payload.columns
? insertRowAt(doc, payload.columns, plan.parentId, plan.index)
: insertBlockAt(doc, payload.type, plan.parentId, plan.index));
if (id) { selected = id; changed(); }
if (id) {
selected = payload.type === 'accordion' ? locate(history.document.blocks, id)?.block.children?.[0]?.id ?? id : id;
if (['accordion', 'accordionitem'].includes(payload.type)) propertyPanel = 'content';
changed();
}
return;
}
const moved = history.change(doc => moveBlockTo(doc, payload.block.id, plan.parentId, plan.index) ? payload.block.id : null);
+19 -8
View File
@@ -5,22 +5,28 @@ export function decorateContainers(preview, content) {
const visit = (blocks, parent = null) => {
blocks.forEach((block, index) => {
const node = preview.getElementById(block.id);
if (node && ['section', 'row', 'column'].includes(block.type)) {
// Editing must expose the body without changing the visitor's saved open/closed state.
if (node && block.type === 'accordionitem') node.open = true;
if (node && ['section', 'row', 'column', 'accordion', 'accordionitem'].includes(block.type)) {
const label = block.type === 'column' && parent?.type === 'row'
? `Spalte ${index + 1} von ${blocks.length}` : blockLabel(block);
? `Spalte ${index + 1} von ${blocks.length}` : block.type === 'accordionitem'
? `Eintrag „${block.props?.title || 'Ohne Titel'}“` : blockLabel(block);
node.classList.add('editor-container');
node.dataset.editorLabel = label;
if (!block.children?.length) {
node.classList.add('editor-empty-container');
const host = block.type === 'section' ? node.querySelector(':scope > .section-inner') : node;
if (!block.children?.length || ['accordion', 'accordionitem'].includes(block.type)) {
const compact = Boolean(block.children?.length);
if (!compact) node.classList.add('editor-empty-container');
const host = block.type === 'section' ? node.querySelector(':scope > .section-inner')
: block.type === 'accordionitem' ? node.querySelector(':scope > .accordion-body') : node;
if (!host) return;
const empty = preview.createElement('div');
empty.className = 'editor-empty';
empty.className = compact ? 'editor-empty editor-add-content' : 'editor-empty';
const name = preview.createElement('span');
name.className = 'editor-empty-name'; name.textContent = label;
const hint = preview.createElement('span');
hint.className = 'editor-empty-hint';
hint.textContent = block.type === 'row' ? 'Spalte hierher ziehen oder hinzufügen' : 'Element hierher ziehen oder hinzufügen';
hint.textContent = block.type === 'row' ? 'Spalte hierher ziehen oder hinzufügen'
: block.type === 'accordion' ? 'Eintrag hinzufügen und darin Inhalte gestalten' : 'Element hierher ziehen oder hinzufügen';
const actions = preview.createElement('div'); actions.className = 'editor-empty-actions';
const add = (text, attribute, value, accessibleName) => {
const button = preview.createElement('button');
@@ -31,10 +37,15 @@ export function decorateContainers(preview, content) {
actions.append(button);
};
add(block.type === 'row' ? '+ Spalte hinzufügen' : '+ Inhalt hinzufügen', 'data-editor-insert', '', `${label}: ${block.type === 'row' ? 'Spalte' : 'Inhalt'} hinzufügen`);
if (block.type === 'accordion') {
actions.replaceChildren();
add('+ Eintrag hinzufügen', 'data-editor-add-entry', '', 'Akkordeon: Eintrag hinzufügen');
}
if (block.type === 'section') {
for (const count of [1, 2, 3, 4]) add(`${count} ${count === 1 ? 'Spalte' : 'Spalten'}`, 'data-editor-row', String(count), `${count} ${count === 1 ? 'Spalte' : 'Spalten'} in dieser Sektion anlegen`);
}
empty.append(name, hint, actions);
if (!compact) empty.append(name, hint);
empty.append(actions);
host.append(empty);
}
}
+2
View File
@@ -20,4 +20,6 @@
.editor-container:hover:not(.editor-selected),.editor-dragging .editor-container{outline:1px dashed #6b927a;outline-offset:-1px}
.editor-container.editor-insert-target{outline:3px solid #b55b12!important;outline-offset:-3px}
.editor-dragging .editor-empty{background:#e6f1e9;border-color:#467858}
.editor-add-content{min-height:0;margin-top:12px;padding:10px;border-width:1px;background:#f3f8f4}
.block-accordion.editor-container>.editor-add-content{margin-top:16px}
@media(max-width:650px){.editor-empty{padding:16px 10px}.editor-empty button{min-height:44px}}