fix: expose accordion contents and add clear insert actions (1.8.3)
This commit is contained in:
1 parent
a108e7d143
commit
a3029966cb
12 files changed
+257
-18
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}}
|
||||
Reference in new issue
Block a user