PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
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 0.6.0 0.7.0 All 126 releases
extendify / app / QuickEdit / Services / NavRefTagger.php

NavRefTagger.php in Extendify 3.1.5, at app/QuickEdit/Services/NavRefTagger.php

107 lines 3.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Extendify\QuickEdit\Services;
4
5 defined('ABSPATH') || die('No direct access.');
6
7 use Extendify\Config;
8
9 // Surfaces the wp_navigation ref + per-item index so the front-end can
10 // route nav-item edits to WPNavigationController. Without this, nav items
11 // inside ref-based navigations 409 on save (the parsed core/navigation has
12 // empty innerBlocks; items live in a separate wp_navigation CPT).
13 class NavRefTagger
14 {
15 // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound -- 7.0 floor: no const visibility
16 const ATTR_REF = 'data-extendify-quick-edit-nav-ref';
17 // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound -- 7.0 floor: no const visibility
18 const ATTR_ITEM_INDEX = 'data-extendify-quick-edit-nav-item-index';
19
20 // Stack so nested navigations (submenus) get their own counter.
21 private static $navStack = [];
22
23 public static function init()
24 {
25 add_action('template_redirect', [self::class, 'reset']);
26 add_filter('pre_render_block', [self::class, 'onPreRenderBlock'], 10, 2);
27 // Priority 9 runs before TagTemplateParts (10) so the agent's first-tag
28 // matcher isn't thrown off by our attr on the nav wrapper.
29 add_filter('render_block', [self::class, 'onRenderBlock'], 9, 2);
30 }
31
32 public static function reset()
33 {
34 self::$navStack = [];
35 }
36
37 public static function onRenderBlock(string $html, array $block): string
38 {
39 if (is_admin() || !is_string($html) || $html === '') {
40 return $html;
41 }
42 if (!is_user_logged_in() || !current_user_can(Config::$requiredCapability)) {
43 return $html;
44 }
45
46 $name = $block['blockName'] ?? '';
47
48 if ($name === 'core/navigation') {
49 $ref = (int) ($block['attrs']['ref'] ?? 0);
50 $html = self::tagFirstElement(
51 $html,
52 self::ATTR_REF,
53 $ref > 0 ? (string) $ref : ''
54 );
55 // Pop after tagging; inner items have all rendered by now.
56 if (!empty(self::$navStack)) {
57 array_pop(self::$navStack);
58 }
59 return $html;
60 }
61
62 if ($name === 'core/navigation-link' || $name === 'core/navigation-submenu') {
63 if (empty(self::$navStack)) {
64 return $html;
65 }
66 $top = count(self::$navStack) - 1;
67 $idx = self::$navStack[$top]['counter'];
68 self::$navStack[$top]['counter']++;
69 $html = self::tagFirstElement($html, self::ATTR_ITEM_INDEX, (string) $idx);
70 return $html;
71 }
72
73 return $html;
74 }
75
76 // Push the frame BEFORE the navigation's render_callback runs so
77 // inner items can find it on render_block.
78 public static function onPreRenderBlock($pre, array $parsed_block)
79 {
80 if (($parsed_block['blockName'] ?? '') !== 'core/navigation') {
81 return $pre;
82 }
83 $ref = (int) ($parsed_block['attrs']['ref'] ?? 0);
84 self::$navStack[] = [
85 'ref' => $ref,
86 'counter' => 0,
87 ];
88 return $pre; // never short-circuit
89 }
90
91 private static function tagFirstElement(string $html, string $attr, string $value): string
92 {
93 if ($value === '') {
94 return $html;
95 }
96 $tp = new \WP_HTML_Tag_Processor($html);
97 if ($tp->next_tag()) {
98 if ($tp->get_attribute($attr) !== null) {
99 return $html;
100 }
101 $tp->set_attribute($attr, $value);
102 return $tp->get_updated_html();
103 }
104 return $html;
105 }
106 }
107