'object', 'properties' => array_merge(DiviSchema::write(), [ 'changes' => [ 'type' => 'array', 'minItems' => 1, 'maxItems' => 50, 'items' => [ 'type' => 'object', 'properties' => [ 'id' => ['type' => 'string'], 'set' => ['type' => 'object', 'description' => 'Kurzform-Felder, z. B. {"text": "Neu"}.'], 'attrs' => [ 'type' => 'object', 'description' => 'Vollform: Attributpfad → Breakpoint-Werte.', ], 'preset' => PresetAssignment::valueSchema( true, 'Preset-Name oder -ID, Liste (Stapel, 1–' . PresetAssignment::MAX_STACK . ') oder null zum Entfernen. Bei gleicher Priorität gewinnt das letzte ' . 'Preset im Stapel.' ), 'group_presets' => [ 'type' => 'object', 'description' => 'Gruppen-Presets: {Gruppe: Preset-Name | Liste | null}. Gruppe = ' . 'Kurzname (font-body), voller Name (divi/font-body) oder Attributpfad; null ' . 'entfernt den Eintrag der Gruppe.', 'additionalProperties' => PresetAssignment::valueSchema(true), ], 'clear_overrides' => [ 'type' => 'boolean', 'description' => 'true entfernt lokale Werte, die Werte der zugewiesenen Presets ' . 'überdecken (nach set und attrs).', ], 'label' => [ 'type' => ['string', 'null'], 'description' => 'Admin-Label im Divi-Builder; null entfernt.', ], ], 'required' => ['id'], 'additionalProperties' => false, ], ], ]), 'required' => ['post_id', 'tree_rev', 'changes'], 'additionalProperties' => false, ]; } public function annotations(): array { return Annotations::write(true); } protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error { $changes = is_array($input['changes'] ?? null) ? array_values($input['changes']) : []; if ($changes === []) { return Errors::invalidInput('Keine Änderungen angegeben; nichts wurde gespeichert.', [ 'changes' => 'mindestens eine Änderung', ]); } $composer = $composition->composer(); $errors = []; /** @var list $invalidPaths */ $invalidPaths = []; /** @var array $seen id => position of its first change */ $seen = []; /** @var list, 2: string}> $updates */ $updates = []; foreach ($changes as $position => $change) { $field = sprintf('changes[%d]', $position); if (! is_array($change)) { $errors[$field] = 'Objekt mit id erwartet'; continue; } $id = is_string($change['id'] ?? null) ? $change['id'] : ''; if ($id !== '' && isset($seen[$id])) { // Ruling S14: a second change of the same node would overwrite the first silently. $errors[$field . '.id'] = sprintf( 'Knoten "%s" steht schon in changes[%d]; alle Änderungen eines Knotens in einer Änderung bündeln', $id, $seen[$id] ); continue; } $block = $id === '' ? null : $page->index->find($id); if ($id !== '') { $seen[$id] = $position; } if ($block === null) { $errors[$field . '.id'] = sprintf('Knoten "%s" gibt es nicht (lit-divi-outline neu lesen)', $id); continue; } $result = $composer->update($block, $change, $field); $errors += $result['errors']; array_push($invalidPaths, ...$result['invalid_paths']); $updates[] = [$block, $result['attrs'], $field]; } if ($errors === []) { $errors = ColumnWidths::updateErrors($page->index, $updates); } if ($errors !== []) { return DiviErrors::composed( ['errors' => $errors, 'invalid_paths' => $invalidPaths], 'Änderungen sind ungültig; nichts wurde gespeichert.' ); } $updates = array_values(array_filter( $updates, static fn (array $update): bool => $update[1] !== $update[0]->attrs() )); $change = new DiviChange(sprintf('%d Divi-Knoten geändert', count($updates))); foreach ($updates as [$block, $attrs]) { // Composer::update() output: every client value already passed the choke point. $block->setAttrs($attrs); $change->record($block, 'updated'); } return $change; } }