PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
← All changes | app/Agent/Controllers/UpdateBlocksController.php +79 -2 3.2.1 → 3.2.2 View file →
@@ -108,8 +108,10 @@
108 108 $blockId,
109 109 $operation,
110 110 $trees[$owner]['wrappers']
111 111 );
112 + } elseif ($op === 'duplicate') {
113 + $reason = self::applyDuplicate($trees[$owner]['blocks'], $blockId, $operation);
112 114 } elseif (in_array($op, ['edit', 'delete', 'move'], true)) {
113 115 $reason = self::applyOperation($trees[$owner]['blocks'], $op, $blockId, $operation);
114 116 } else {
115 117 $reason = 'unknown op';
@@ -296,10 +298,10 @@
296 298 // Returns null when the block spliced in next to its anchor, or the refusal reason.
297 299 private static function applyAdd(array &$blocks, int $anchorId, array $operation, array &$sharedWrappers)
298 300 {
299 301 $position = (string) ($operation['position'] ?? '');
300 - if (!in_array($position, ['before', 'after'], true)) {
301 - return "position must be 'before' or 'after'";
302 + if (!in_array($position, ['before', 'after', 'inside'], true)) {
303 + return "position must be 'before', 'after' or 'inside'";
302 304 }
303 305 $newBlock = self::parseSingleBlock((string) ($operation['block'] ?? ''));
304 306 if (!$newBlock) {
305 307 return 'block must parse to exactly one block';
@@ -307,8 +309,11 @@
307 309 $anchorPath = PostBlockFinder::pathByRef($blocks, $anchorId);
308 310 if ($anchorPath === null) {
309 311 return 'anchor block not found in this post';
310 312 }
313 + if ($position === 'inside') {
314 + return self::nestInMenuItem($blocks, $anchorPath, $newBlock);
315 + }
311 316 // A bare column is only valid as a core/columns child, so its anchor
312 317 // decides the splice here — the code owns the wrapper, never the model.
313 318 if (($newBlock['blockName'] ?? '') === 'core/column') {
314 319 return self::spliceColumn($blocks, $anchorPath, $newBlock, $position, $anchorId, $sharedWrappers);
@@ -316,8 +321,35 @@
316 321 $blocks = self::insertAtPath($blocks, $anchorPath, $newBlock, $position);
317 322 return null;
318 323 }
319 324
325 + // The editor's "Add submenu": the link becomes a submenu keeping its label and URL.
326 + private static function nestInMenuItem(array &$blocks, array $anchorPath, array $newBlock)
327 + {
328 + $anchor = self::blockAtPath($blocks, $anchorPath);
329 + $anchorName = $anchor['blockName'] ?? '';
330 + if (!in_array($anchorName, ['core/navigation-link', 'core/navigation-submenu'], true)) {
331 + return "position 'inside' needs a menu item as the anchor";
332 + }
333 + if (($newBlock['blockName'] ?? '') !== 'core/navigation-link') {
334 + return 'only a menu link can go inside a menu item';
335 + }
336 + // A child claiming top level renders as if it sat beside its parent.
337 + unset($newBlock['attrs']['isTopLevelLink']);
338 + if ($anchorName === 'core/navigation-link') {
339 + $anchor['blockName'] = 'core/navigation-submenu';
340 + $attrs = [];
341 + foreach (($anchor['attrs'] ?? []) as $key => $value) {
342 + $attrs[$key === 'isTopLevelLink' ? 'isTopLevelItem' : $key] = $value;
343 + }
344 + $anchor['attrs'] = $attrs;
345 + }
346 + $anchor['innerBlocks'][] = $newBlock;
347 + $anchor['innerContent'][] = null;
348 + $blocks = self::spliceAtPath($blocks, $anchorPath, $anchor);
349 + return null;
350 + }
351 +
320 352 // A pre-existing section never absorbs an add — the only wrapper an add
321 353 // joins is one this same batch created.
322 354 private static function spliceColumn(
323 355 array &$blocks,
@@ -422,8 +454,53 @@
422 454 $blocks = self::insertAtPath($blocks, $lastChild, $column, 'after');
423 455 $sharedWrappers[$blockId . ':after'] = $shellRef;
424 456 $sharedWrappers[$blockId . ':before'] = $shellRef;
425 457 return true;
458 + }
459 +
460 + // Returns null when the copy spliced in, or the refusal reason.
461 + private static function applyDuplicate(array &$blocks, int $blockId, array $operation)
462 + {
463 + $path = PostBlockFinder::pathByRef($blocks, $blockId);
464 + if ($path === null) {
465 + return 'block id not found in this post';
466 + }
467 + $original = self::blockAtPath($blocks, $path);
468 + $copy = self::withoutStamps($original);
469 + if (isset($operation['block'])) {
470 + $copy = self::parseSingleBlock((string) $operation['block']);
471 + if (!$copy) {
472 + return 'block must parse to exactly one block';
473 + }
474 + if (($copy['blockName'] ?? '') !== ($original['blockName'] ?? '')) {
475 + return 'a copy must keep the original block type';
476 + }
477 + }
478 + $anchorPath = $path;
479 + $position = 'after';
480 + if (isset($operation['targetId'])) {
481 + $anchorPath = PostBlockFinder::pathByRef($blocks, (int) $operation['targetId']);
482 + if ($anchorPath === null) {
483 + return 'target block not found in this post';
484 + }
485 + if (array_slice($anchorPath, 0, count($path)) === $path && $anchorPath !== $path) {
486 + return 'target is inside the copied block';
487 + }
488 + $position = (string) ($operation['position'] ?? 'after');
489 + if (!in_array($position, ['before', 'after'], true)) {
490 + return "position must be 'before' or 'after'";
491 + }
492 + }
493 + $blocks = self::insertAtPath($blocks, $anchorPath, $copy, $position);
494 + return null;
495 + }
496 +
497 + // A stamped copy would catch later ops aimed at the original's ids.
498 + private static function withoutStamps(array $block): array
499 + {
500 + unset($block[PostBlockFinder::REF_KEY]);
501 + $block['innerBlocks'] = array_map([self::class, 'withoutStamps'], $block['innerBlocks'] ?? []);
502 + return $block;
426 503 }
427 504
428 505 private static function blockAtPath(array $blocks, array $path)
429 506 {