Fassung 1.5.1: der Werkzeugkasten kennt den Zusammenhang
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

Umuts Einwand: Zeile und Spalte standen oben in der Auswahl, als waeren sie
Seitenanfaenge. Sie gehoeren in den Inhalt - erst eine Sektion, dann eine Zeile
darin. Das war nicht nur eine Anzeigefrage: eine Zeile nahm bisher jedes Kind
an, eine Ueberschrift konnte neben den Spalten landen, und eine Spalte durfte
ueberall stehen. Die Struktur, die der Editor in seinem eigenen Hinweistext
beschreibt, galt fuer die Schaltflaechen nicht.

Uebernommen ist Flatsomes Regelmodell (Elements.php): ALLOW - eine Zeile nimmt
nur Spalten; REQUIRE - eine Spalte lebt nur in einer Zeile. Dazu ein Zusatz,
den Flatsome nicht hat: ROOT_NEEDS_CONTAINER - eine Zeile ist kein
Seitenanfang. In jedem Behaelter bleibt sie erlaubt, auch in einem Tab.

Daraus die Bedienung:

- Werkzeugkasten nach Aufbau gruppiert statt flacher Liste aus 31 Typen.
- Was nicht hingehoert, ist gesperrt und sagt beim Ueberfahren warum. Eine
  Regel, die man erklaert bekommt, lernt man; eine Schaltflaeche, die nur
  nicht mehr reagiert, drueckt man weiter.
- Was moeglich ist, sagt wohin es kommt.
- Die Zeile wird nach Spaltenzahl angeboten und bringt ihre Spalten mit; der
  separate Zeilenbereich ist entfallen.
- insertionTarget beantwortet "wohin kaeme das?", ohne etwas einzufuegen.
  Knopf, Einfuegepunkt, Ziehen und die Sperrlogik nutzen dieselbe Funktion -
  vorher entschied insertBlock allein, und insertRow hatte eine zweite Kopie
  derselben Platzierungslogik.

Ein Element, das der gewaehlte Behaelter nicht nimmt, wird nicht gesperrt,
sondern steigt auf, bis ein Elternteil es annimmt.

Nebenbei behoben: postnav fehlte seit Schema v18 in labels und icons, der
Strukturbaum zeigte dafuer "Unbekannt" und ein Fragezeichen.

Geprueft: npm test 27/27, phpunit 446/446, npm run test:editor vollstaendig
mit neuem Abschnitt und ohne axe-Verstoss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-14 07:13:25 +02:00
1 parent 22e4ada830
commit 0e2334d30f
8 files changed
+397 -74

No files matched your search

+68 -1
View File
@@ -1,7 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { EditorHistory, createBlock, locate, insertBlock, insertBlockAt, moveBlockTo, dropList, duplicateBlock, moveBlock, upgradeDocument, effectiveDesign, setDesign, insertPreset, insertRow, blockPath, insertMedia, setEffect, isPluginBlock, blockLabel, blockIcon, canContain, isContainer, setStyle, effectiveStyle, hoverStyle, resetStyleDevice, clearStyle, copyStyle, SCHEMA_VERSION } from '../public/assets/editor-model.js';
import { EditorHistory, createBlock, locate, insertBlock, insertBlockAt, moveBlockTo, dropList, insertionTarget, insertRowAt, CATEGORIES, labels as blockLabels, duplicateBlock, moveBlock, upgradeDocument, effectiveDesign, setDesign, insertPreset, insertRow, blockPath, insertMedia, setEffect, isPluginBlock, blockLabel, blockIcon, canContain, isContainer, setStyle, effectiveStyle, hoverStyle, resetStyleDevice, clearStyle, copyStyle, SCHEMA_VERSION } from '../public/assets/editor-model.js';
import { readFile } from 'node:fs/promises';
import { designCss } from '../scripts/build-design.mjs';
@@ -425,3 +425,70 @@ test('the top level takes everything the validator takes there, and nothing else
// Beim Ziehen aus dem Werkzeugkasten gibt es den Block noch nicht.
assert.doesNotThrow(() => dropList(document, 's1', null));
});
// --- Sektion, Zeile, Spalte, Inhalt ----------------------------------------------------------
// Die Struktur, die der Editor in seinem eigenen Hinweistext beschreibt, gilt jetzt auch fuer
// die Schaltflaechen: eine Zeile nahm bisher jedes Kind, eine Spalte stand ueberall.
test('a row takes columns and nothing else, a column only lives in a row', () => {
const document = fixture();
const rowId = insertRow(document, 3, 's2');
const row = locate(document.blocks, rowId).block;
assert.equal(row.children.length, 3);
assert.ok(row.children.every(child => child.type === 'column'));
// Eine Ueberschrift gehoert nicht neben die Spalten, sondern in eine davon.
assert.equal(insertBlockAt(document, 'heading', rowId, 0), null);
assert.equal(row.children.length, 3);
assert.ok(insertBlockAt(document, 'heading', row.children[0].id, 0));
// Eine Spalte gehoert nicht in eine Sektion.
assert.equal(insertBlockAt(document, 'column', 's2', 0), null);
assert.ok(insertBlockAt(document, 'column', rowId, 3));
});
test('nothing that needs a container is offered at the top of a document', () => {
const document = fixture();
for (const type of ['row', 'column', 'accordionitem', 'tabitem']) {
assert.equal(insertionTarget(document, null, type), null, type);
assert.equal(insertBlock(document, type, null), null, type);
}
// Eine Sektion dagegen schon - damit faengt eine Seite an.
assert.ok(insertBlock(document, 'section', null));
});
test('the toolbox is told the same place the click will use', () => {
const document = fixture();
upgradeDocument(document, SCHEMA_VERSION);
const rowId = insertRow(document, 2, 's2');
const columnId = locate(document.blocks, rowId).block.children[0].id;
// In einer Zeile ist nur eine Spalte moeglich, und sie landet in der Zeile.
assert.deepEqual(insertionTarget(document, rowId, 'column'), { parentId: rowId, index: 2 });
// Eine Ueberschrift nimmt die Zeile nicht - sie steigt auf und landet hinter der Zeile in der
// Sektion. Sperren waere unnoetig: die Absicht ist eindeutig, nur die Zeile ist der falsche Ort.
assert.deepEqual(insertionTarget(document, rowId, 'heading'), { parentId: 's2', index: 1 });
// In einer Spalte ist Inhalt moeglich, und er landet in der Spalte.
assert.equal(insertionTarget(document, columnId, 'heading').parentId, columnId);
// Eine weitere Spalte neben der gewaehlten: eine Ebene hoeher, direkt dahinter.
assert.deepEqual(insertionTarget(document, columnId, 'column'), { parentId: rowId, index: 1 });
// Und die Auskunft stimmt mit der Tat ueberein.
for (const [selection, type] of [[rowId, 'column'], [columnId, 'heading'], [columnId, 'column'], ['s2', 'row']]) {
const promised = insertionTarget(document, selection, type);
const id = insertBlock(document, type, selection);
assert.equal(locate(document.blocks, id).parent?.id ?? null, promised.parentId, `${type} aus ${selection}`);
}
});
test('every block type appears in exactly one toolbox group', () => {
const listed = CATEGORIES.flatMap(category => category.types);
assert.equal(listed.length, new Set(listed).size, 'kein Typ doppelt');
assert.deepEqual([...listed].sort(), Object.keys(blockLabels).sort());
});
test('a row dropped at an exact position brings its columns along', () => {
const document = fixture();
assert.ok(insertRowAt(document, 4, 's1', 0));
const row = locate(document.blocks, 's1').block.children[0];
assert.equal(row.type, 'row');
assert.equal(row.children.length, 4);
// Und in eine Zeile hinein gibt es keine zweite Zeile.
assert.equal(insertRowAt(document, 2, row.id, 0), null);
});