fix: builder column classes only – no 4_5, all breakpoints checked (S49)
New rows no longer accept 4_5 (the builder does not offer it). On update, the flexType of every non-desktop breakpoint (tablet, phone, …) is format-checked against the builder list like desktop. Guide updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -25,8 +25,8 @@ Neue Divi-Inhalte beschreibt man als JSON-Knoten. `lit-divi-insert` (in `nodes`)
|
|||||||
`section_type: "fullwidth"` für Vollbreite-Abschnitte (dann nur `fullwidth-…`-Module in `modules`);
|
`section_type: "fullwidth"` für Vollbreite-Abschnitte (dann nur `fullwidth-…`-Module in `modules`);
|
||||||
`background_color`. Spezialabschnitte (specialty) nur im Divi-Builder bauen.
|
`background_color`. Spezialabschnitte (specialty) nur im Divi-Builder bauen.
|
||||||
- **Zeile** (`row`): `columns` = Breiten als Brüche, die zusammen 1 ergeben (`"4_4"`, `"1_2,1_2"`,
|
- **Zeile** (`row`): `columns` = Breiten als Brüche, die zusammen 1 ergeben (`"4_4"`, `"1_2,1_2"`,
|
||||||
`"1_3,2_3"`, `"1_4,1_4,1_2"`, auch Fünftel); `modules` = je Spalte eine Liste (bei einer Spalte reicht eine
|
`"1_3,2_3"`, `"1_4,1_4,1_2"`, auch `1_5`, `2_5`, `3_5` wie im Divi-Builder – `4_5` nicht); `modules` = je
|
||||||
einfache Liste).
|
Spalte eine Liste (bei einer Spalte reicht eine einfache Liste).
|
||||||
- **Module** mit Kind-Modulen (Akkordeon, Tabs, Slider, Kontaktformular, Karte, Social-Media-Follow,
|
- **Module** mit Kind-Modulen (Akkordeon, Tabs, Slider, Kontaktformular, Karte, Social-Media-Follow,
|
||||||
Icon-Liste) bekommen ihre Einträge in `items`; der Eintragstyp ergibt sich von selbst.
|
Icon-Liste) bekommen ihre Einträge in `items`; der Eintragstyp ergibt sich von selbst.
|
||||||
- `label` fehlt → Admin-Label „<Abschnitt> – <Modul>“, z. B. „Hero – Button“. Abschnitten deshalb ein Label
|
- `label` fehlt → Admin-Label „<Abschnitt> – <Modul>“, z. B. „Hero – Button“. Abschnitten deshalb ein Label
|
||||||
@@ -90,7 +90,9 @@ Fehler: `validation_failed` bzw. `invalid_attr_path` mit `fields` (Eingabepfad
|
|||||||
|
|
||||||
Die Breite einer Spalte steht in `module.decoration.sizing` (Desktop: `flexType`, z. B. `8_24` = 1/3). Wer
|
Die Breite einer Spalte steht in `module.decoration.sizing` (Desktop: `flexType`, z. B. `8_24` = 1/3). Wer
|
||||||
diesen Desktop-Wert über `attrs` ersetzt, ersetzt das ganze Objekt: `flexType` immer mitschicken, sonst fällt
|
diesen Desktop-Wert über `attrs` ersetzt, ersetzt das ganze Objekt: `flexType` immer mitschicken, sonst fällt
|
||||||
die Breite weg. In einer Zeile mit festen Breiten alle Spalten zusammen ändern (Summe 1). Die Angabe
|
die Breite weg. Erlaubt sind je Breakpoint (Desktop, Tablet, Telefon, …) nur die Spaltenklassen des
|
||||||
|
Divi-Builders: `1_24` … `24_24`, `1_5`, `2_5`, `3_5`, `1_7`, `1_9`, `1_10`, `1_11` oder `none`. In einer Zeile
|
||||||
|
mit festen Breiten alle Spalten zusammen ändern (Summe 1). Die Angabe
|
||||||
`flexColumnStructure` der Zeile (Spaltenvorlage im Divi-Builder) passt das Werkzeug nicht an; nach einer
|
`flexColumnStructure` der Zeile (Spaltenvorlage im Divi-Builder) passt das Werkzeug nicht an; nach einer
|
||||||
Breitenänderung zeigt der Builder dort evtl. noch die alte Vorlage; gespeichert sind die neuen Breiten (bei
|
Breitenänderung zeigt der Builder dort evtl. noch die alte Vorlage; gespeichert sind die neuen Breiten (bei
|
||||||
Bedarf die Vorlage im Builder neu wählen).
|
Bedarf die Vorlage im Builder neu wählen).
|
||||||
|
|||||||
@@ -10,13 +10,16 @@ use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Column widths of Divi rows (pure, static): the "columns" input of new rows
|
* Column widths of Divi rows (pure, static): the "columns" input of new rows
|
||||||
* ("1_3,2_3" → flexTypes, ruling S34) and the desktop flexType checks on update (rulings S41,
|
* ("1_3,2_3" → flexTypes, ruling S34) and the flexType checks on update (rulings S41, S43,
|
||||||
* S43). Every width this class returns is built from digits or taken from a fixed list.
|
* S49). Every width this class returns is built from digits or taken from a fixed list.
|
||||||
*/
|
*/
|
||||||
final class ColumnWidths
|
final class ColumnWidths
|
||||||
{
|
{
|
||||||
/** Column fractions Divi's flex layout knows besides multiples of 1/24. */
|
/**
|
||||||
private const SPECIAL_WIDTHS = ['1_5', '2_5', '3_5', '4_5', '1_7', '1_9', '1_10', '1_11'];
|
* Column fractions of "columns" besides multiples of 1/24: the builder's special column
|
||||||
|
* classes (ruling S49 – no 4_5, the builder does not offer it).
|
||||||
|
*/
|
||||||
|
private const SPECIAL_WIDTHS = ['1_5', '2_5', '3_5', '1_7', '1_9', '1_10', '1_11'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The builder's "Column Class" choices (Divi 5.2.1 visual-builder/build/field-library.js):
|
* The builder's "Column Class" choices (Divi 5.2.1 visual-builder/build/field-library.js):
|
||||||
@@ -29,6 +32,8 @@ final class ColumnWidths
|
|||||||
/** Divi's column default (column/module-default-render-attributes.json). */
|
/** Divi's column default (column/module-default-render-attributes.json). */
|
||||||
private const DEFAULT_WIDTH = '24_24';
|
private const DEFAULT_WIDTH = '24_24';
|
||||||
|
|
||||||
|
private const CLASS_LIST = '1_24 … 24_24, 1_5, 2_5, 3_5, 1_7, 1_9, 1_10, 1_11 oder none';
|
||||||
|
|
||||||
private const FORMAT = 'Spaltenaufteilung wie "1_2,1_2" oder "1_3,2_3" erwartet';
|
private const FORMAT = 'Spaltenaufteilung wie "1_2,1_2" oder "1_3,2_3" erwartet';
|
||||||
|
|
||||||
private const FULL_ROW = 'die Spaltenbreiten müssen zusammen eine volle Zeile ergeben (z. B. "1_3,2_3")';
|
private const FULL_ROW = 'die Spaltenbreiten müssen zusammen eine volle Zeile ergeben (z. B. "1_3,2_3")';
|
||||||
@@ -100,12 +105,13 @@ final class ColumnWidths
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Column widths on update (rulings S41, S43). Every new desktop flexType must be one of the
|
* Column widths on update (rulings S41, S43, S49). Every new flexType – desktop and every
|
||||||
* builder's column classes (isColumnClass()) or null (removed: Divi's default 24_24). The
|
* other breakpoint – must be one of the builder's column classes (isColumnClass()) or null
|
||||||
* row sum is only checked for rows that are exact today – every column has a class other
|
* (removed; desktop then is Divi's default 24_24). The desktop row sum is only checked for
|
||||||
* than "none", the classes fill the row (rowError()), the row does not wrap or use a grid
|
* rows that are exact today – every column has a class other than "none", the classes fill
|
||||||
* on desktop – and when no new width is "none". Such a row must stay exact after all
|
* the row (rowError()), the row does not wrap or use a grid on desktop – and when no new
|
||||||
* changes; other rows are laid out by Divi as the builder allows.
|
* width is "none". Such a row must stay exact after all changes; other rows are laid out by
|
||||||
|
* Divi as the builder allows.
|
||||||
*
|
*
|
||||||
* @param list<array{0: Block, 1: array<string, mixed>, 2: string}> $updates Block, its new attrs, input path.
|
* @param list<array{0: Block, 1: array<string, mixed>, 2: string}> $updates Block, its new attrs, input path.
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
@@ -116,14 +122,27 @@ final class ColumnWidths
|
|||||||
$new = [];
|
$new = [];
|
||||||
$rows = [];
|
$rows = [];
|
||||||
foreach ($updates as [$block, $attrs, $field]) {
|
foreach ($updates as [$block, $attrs, $field]) {
|
||||||
|
if ($block->name() !== 'divi/column') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$breakpoint = self::invalidBreakpoint($block->attrs(), $attrs);
|
||||||
|
if ($breakpoint !== null) {
|
||||||
|
$errors[$field . '.attrs.' . self::SIZING] = sprintf(
|
||||||
|
'Spaltenbreite (flexType) für %s wie im Divi-Builder erwartet: %s',
|
||||||
|
$breakpoint,
|
||||||
|
self::CLASS_LIST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$width = self::width($attrs);
|
$width = self::width($attrs);
|
||||||
if ($block->name() !== 'divi/column' || $width === self::width($block->attrs())) {
|
if ($width === self::width($block->attrs())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($width !== null && ! self::isColumnClass($width)) {
|
if ($width !== null && ! self::isColumnClass($width)) {
|
||||||
$errors[$field . '.attrs.' . self::SIZING] = 'Spaltenbreite (flexType) wie im Divi-Builder erwartet: '
|
$errors[$field . '.attrs.' . self::SIZING] = 'Spaltenbreite (flexType) wie im Divi-Builder erwartet: '
|
||||||
. '1_24 … 24_24, 1_5, 2_5, 3_5, 1_7, 1_9, 1_10, 1_11 oder none';
|
. self::CLASS_LIST;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,10 +191,34 @@ final class ColumnWidths
|
|||||||
return self::rowError($widths) === null;
|
return self::rowError($widths) === null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $attrs */
|
/**
|
||||||
private static function width(array $attrs): mixed
|
* The first non-desktop breakpoint whose flexType changed to something that is no column
|
||||||
|
* class (ruling S49), or null.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $before
|
||||||
|
* @param array<string, mixed> $after
|
||||||
|
*/
|
||||||
|
private static function invalidBreakpoint(array $before, array $after): ?string
|
||||||
{
|
{
|
||||||
$sizing = AttrMap::desktop($attrs, self::SIZING);
|
foreach (AttrMap::BREAKPOINTS as $breakpoint) {
|
||||||
|
if ($breakpoint === 'desktop') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$width = self::width($after, $breakpoint);
|
||||||
|
if ($width !== null && $width !== self::width($before, $breakpoint) && ! self::isColumnClass($width)) {
|
||||||
|
return $breakpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $attrs */
|
||||||
|
private static function width(array $attrs, string $breakpoint = 'desktop'): mixed
|
||||||
|
{
|
||||||
|
$map = AttrMap::get($attrs, self::SIZING);
|
||||||
|
$sizing = is_array($map) && is_array($map[$breakpoint] ?? null) ? ($map[$breakpoint]['value'] ?? null) : null;
|
||||||
|
|
||||||
return is_array($sizing) ? ($sizing['flexType'] ?? null) : null;
|
return is_array($sizing) ? ($sizing['flexType'] ?? null) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ final class ColumnWidthsTest extends TestCase
|
|||||||
self::assertStringContainsString('volle Zeile', (string) ColumnWidths::parse('1_2,1_3')['error']);
|
self::assertStringContainsString('volle Zeile', (string) ColumnWidths::parse('1_2,1_3')['error']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ruling S49: new rows use only widths the builder offers – 4_5 is no builder column class. */
|
||||||
|
public function testFourFifthsIsNoInsertWidth(): void
|
||||||
|
{
|
||||||
|
self::assertSame(['widths' => ['24_24'], 'error' => 'Spaltenbreite 4_5 gibt es in Divi nicht'], ColumnWidths::parse('1_5,4_5'));
|
||||||
|
self::assertNotNull(ColumnWidths::rowError(['1_5', '4_5']));
|
||||||
|
self::assertSame(['widths' => ['1_5', '2_5', '2_5'], 'error' => null], ColumnWidths::parse('1_5,2_5,2_5'));
|
||||||
|
}
|
||||||
|
|
||||||
/** Ruling S43: the builder's column classes. */
|
/** Ruling S43: the builder's column classes. */
|
||||||
public function testColumnClassesAreTheBuilderList(): void
|
public function testColumnClassesAreTheBuilderList(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -157,6 +157,38 @@ final class DiviUpdateAbilityTest extends DiviWriteAbilityTestCase
|
|||||||
self::assertSame([], $this->saved);
|
self::assertSame([], $this->saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ruling S49: tablet/phone (every non-desktop breakpoint) flexTypes are format-checked like desktop. */
|
||||||
|
public function testUpdateChecksTheColumnClassFormatOnOtherBreakpoints(): void
|
||||||
|
{
|
||||||
|
foreach (['tablet', 'phone', 'phoneWide'] as $breakpoint) {
|
||||||
|
foreach (['4_5', '13_25', 'auto'] as $width) {
|
||||||
|
$result = $this->update()->execute(['post_id' => 7, 'tree_rev' => $this->treeRev(), 'changes' => [[
|
||||||
|
'id' => 's0.r0.c0',
|
||||||
|
'attrs' => ['module.decoration.sizing' => [
|
||||||
|
'desktop' => ['flexType' => '8_24'],
|
||||||
|
$breakpoint => ['flexType' => $width],
|
||||||
|
]],
|
||||||
|
]]]);
|
||||||
|
self::assertInstanceOf(WP_Error::class, $result, $breakpoint . ' ' . $width);
|
||||||
|
$message = self::clientError($result)['fields']['changes[0].attrs.module.decoration.sizing'];
|
||||||
|
self::assertStringContainsString($breakpoint, $message);
|
||||||
|
self::assertStringContainsString('none', $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self::assertSame([], $this->saved);
|
||||||
|
|
||||||
|
$valid = $this->update()->execute(['post_id' => 7, 'tree_rev' => $this->treeRev(), 'changes' => [[
|
||||||
|
'id' => 's0.r0.c0',
|
||||||
|
'attrs' => ['module.decoration.sizing' => [
|
||||||
|
'desktop' => ['flexType' => '8_24'],
|
||||||
|
'tablet' => ['flexType' => '24_24'],
|
||||||
|
'phone' => ['flexType' => 'none'],
|
||||||
|
]],
|
||||||
|
]]]);
|
||||||
|
self::assertTrue($valid['ok'], (string) json_encode($valid));
|
||||||
|
self::assertStringContainsString('"tablet":{"value":{"flexType":"24_24"}},"phone":{"value":{"flexType":"none"}}', $this->saved[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/** Ruling S43: rows that are no plain exact row are not sum-checked (none, omitted width, wrap). */
|
/** Ruling S43: rows that are no plain exact row are not sum-checked (none, omitted width, wrap). */
|
||||||
public function testUpdateAcceptsWidthChangesInLooseRows(): void
|
public function testUpdateAcceptsWidthChangesInLooseRows(): void
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user