PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.1
Elementor Website Builder – more than just a page builder v4.3.1
4.3.1 4.3.0 4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 All 454 releases
elementor / modules / mcp / abilities / appliers / v3-node-bridge.php

v3-node-bridge.php in Elementor Website Builder – more than just a page builder 4.3.1, at modules/mcp/abilities/appliers/v3-node-bridge.php

276 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Elementor\Modules\Mcp\Abilities\Appliers;
4
5 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Style_Settings_Index;
6 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Widget_Bridge_Registry;
7 use Elementor\Modules\Mcp\Abilities\Utils\Widget_Context_Helper;
8 use Elementor\Utils;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 /**
15 * Bridges V4-shape MCP inputs (global-class labels, plain CSS strings) onto the V3
16 * settings shape (`_css_classes`, `custom_css`) for allowlisted V3 widgets so the
17 * same LLM contract works for both widget generations.
18 */
19 class V3_Node_Bridge {
20
21 const V3_CUSTOM_CSS_SETTING = 'custom_css';
22 const V3_CSS_CLASSES_SETTING = '_css_classes';
23 const V3_DYNAMIC_SETTING = '__dynamic__';
24
25 const RESPONSIVE_SUFFIXES = [ '_tablet', '_mobile' ];
26
27 const TYPOGRAPHY_SETTING_SUFFIXES = [
28 'typography',
29 'font_family',
30 'font_weight',
31 'font_style',
32 'text_transform',
33 'text_decoration',
34 'font_size',
35 'line_height',
36 'letter_spacing',
37 'word_spacing',
38 ];
39
40 const TYPOGRAPHY_RESPONSIVE_SUFFIXES = [
41 'font_size',
42 'line_height',
43 'letter_spacing',
44 'word_spacing',
45 ];
46
47 /**
48 * Seeds each control's `dynamic.default` into the node's `__dynamic__` settings map when the
49 * caller did not provide one. Without this, controls whose default is a dynamic tag (e.g.
50 * `theme-post-title.title` -> post-title tag) render as their static fallback in the editor
51 * canvas immediately after mutation and only pick up the dynamic value on a full refresh.
52 *
53 * @param array $node Subtree node (by reference).
54 * @param array $controls Widget controls from Widget_Base::get_controls().
55 */
56 public static function seed_dynamic_defaults( array &$node, array $controls ): void {
57 $existing = $node['settings'][ self::V3_DYNAMIC_SETTING ] ?? [];
58
59 foreach ( $controls as $name => $control ) {
60 $dynamic_default = $control['dynamic']['default'] ?? null;
61
62 if ( ! is_string( $dynamic_default ) || '' === $dynamic_default ) {
63 continue;
64 }
65
66 if ( array_key_exists( $name, $existing ) ) {
67 continue;
68 }
69
70 $existing[ $name ] = $dynamic_default;
71 }
72
73 if ( ! empty( $existing ) ) {
74 $node['settings'][ self::V3_DYNAMIC_SETTING ] = $existing;
75 }
76 }
77
78 public static function is_v3_node( array $node ): bool {
79 // V3 non-widget elements (containers/sections) are intentionally not supported at this layer for now.
80 if ( 'widget' !== ( $node['elType'] ?? null ) ) {
81 return false;
82 }
83
84 $type = $node['widgetType'] ?? null;
85
86 return is_string( $type ) && Widget_Context_Helper::is_v3_allowlisted( $type );
87 }
88
89 /**
90 * Writes labels directly to V3's `_css_classes` (space-separated, deduped).
91 * V4 global class labels are the CSS class names themselves.
92 */
93 public static function apply_classes( array &$node, array $labels ): void {
94 $existing = self::split_css_classes( $node['settings'][ self::V3_CSS_CLASSES_SETTING ] ?? '' );
95 $merged = array_values( array_unique( array_merge( $labels, $existing ) ) );
96
97 $node['settings'][ self::V3_CSS_CLASSES_SETTING ] = implode( ' ', $merged );
98 }
99
100 public static function clear_classes( array &$node ): void {
101 unset( $node['settings'][ self::V3_CSS_CLASSES_SETTING ] );
102 }
103
104 /**
105 * Clears all known mapped style settings and custom_css for a V3 widget.
106 * Mirrors V4 replace semantics (wipe existing style before applying a new one).
107 *
108 * @param array<string, mixed> $node Subtree node (by reference).
109 * @param string $widget_type V3 widget type name.
110 * @param array<string, mixed> $widget_config Widget config from Widget_Context_Helper::get_widget_config().
111 */
112 public static function clear_style_settings( array &$node, string $widget_type, array $widget_config = [] ): void {
113 $keys = self::collect_style_setting_keys( $widget_type, $widget_config );
114 $keys[] = self::V3_CUSTOM_CSS_SETTING;
115
116 foreach ( array_unique( $keys ) as $key ) {
117 unset( $node['settings'][ $key ] );
118 }
119 }
120
121 /**
122 * Writes a CSS string into V3's `custom_css`. Plain declaration lists (`color: red;`)
123 * are wrapped in `selector { ... }` — the Pro custom-css module replaces `selector`
124 * with the widget's wrapper at render.
125 *
126 * @return string|null Warning message when Pro is missing, otherwise null.
127 */
128 public static function apply_custom_css( array &$node, string $css_string, string $widget_type = '' ): ?string {
129 $css_string = trim( $css_string );
130
131 if ( '' === $css_string ) {
132 unset( $node['settings'][ self::V3_CUSTOM_CSS_SETTING ] );
133 return null;
134 }
135
136 if ( ! Utils::has_pro() ) {
137 $widget_label = '' !== $widget_type ? $widget_type : esc_html__( 'this V3 widget', 'elementor' );
138
139 return sprintf(
140 /* translators: %s: V3 widget type name. */
141 __( 'V3 widget styles for `%s` require Elementor Pro (Custom CSS module) and were not applied. Do not retry the `style` field for this widget in the current environment — either fall back to `settings`-only edits, or ask the user to install and activate Elementor Pro.', 'elementor' ),
142 $widget_label
143 );
144 }
145
146 $node['settings'][ self::V3_CUSTOM_CSS_SETTING ] = self::wrap_with_selector( $css_string );
147
148 return null;
149 }
150
151 /**
152 * @param string $widget_type V3 widget type name.
153 * @param array<string, mixed> $widget_config Widget config from Widget_Context_Helper::get_widget_config().
154 * @return string[]
155 */
156 private static function collect_style_setting_keys( string $widget_type, array $widget_config ): array {
157 $keys = [];
158 $overrides = V3_Widget_Bridge_Registry::get_style_overrides( $widget_type );
159
160 foreach ( $overrides as $override ) {
161 $keys = array_merge( $keys, self::expand_override_keys( $override ) );
162 }
163
164 $generic = V3_Style_Settings_Index::build( $widget_config['controls'] ?? [], $overrides );
165 foreach ( $generic as $rule ) {
166 $setting = (string) ( $rule['setting'] ?? '' );
167 if ( '' === $setting ) {
168 continue;
169 }
170 $keys[] = $setting;
171 if ( ! empty( $rule['responsive'] ) ) {
172 foreach ( self::RESPONSIVE_SUFFIXES as $suffix ) {
173 $keys[] = $setting . $suffix;
174 }
175 }
176 }
177
178 return $keys;
179 }
180
181 /**
182 * @param array<string, mixed> $override
183 * @return string[]
184 */
185 private static function expand_override_keys( array $override ): array {
186 if ( isset( $override['typography_prefix'] ) ) {
187 return self::expand_typography_keys( (string) $override['typography_prefix'] );
188 }
189
190 if ( isset( $override['border_prefix'] ) ) {
191 return self::expand_border_keys( (string) $override['border_prefix'] );
192 }
193
194 if ( isset( $override['box_shadow_prefix'] ) ) {
195 $prefix = (string) $override['box_shadow_prefix'];
196 return [
197 $prefix . '_box_shadow_type',
198 $prefix . '_box_shadow',
199 ];
200 }
201
202 $setting = $override['setting'] ?? null;
203 if ( ! is_string( $setting ) || '' === $setting ) {
204 return [];
205 }
206
207 $keys = [ $setting ];
208 if ( ! empty( $override['responsive'] ) ) {
209 foreach ( self::RESPONSIVE_SUFFIXES as $suffix ) {
210 $keys[] = $setting . $suffix;
211 }
212 }
213
214 if ( 'box_shadow' === ( $override['resolver'] ?? null ) ) {
215 $keys[] = $setting . '_type';
216 }
217
218 return $keys;
219 }
220
221 /**
222 * @return string[]
223 */
224 private static function expand_typography_keys( string $prefix ): array {
225 $keys = [];
226
227 foreach ( self::TYPOGRAPHY_SETTING_SUFFIXES as $suffix ) {
228 $keys[] = $prefix . '_' . $suffix;
229 if ( ! in_array( $suffix, self::TYPOGRAPHY_RESPONSIVE_SUFFIXES, true ) ) {
230 continue;
231 }
232 foreach ( self::RESPONSIVE_SUFFIXES as $responsive_suffix ) {
233 $keys[] = $prefix . '_' . $suffix . $responsive_suffix;
234 }
235 }
236
237 return $keys;
238 }
239
240 /**
241 * @return string[]
242 */
243 private static function expand_border_keys( string $prefix ): array {
244 $keys = [
245 $prefix . '_border',
246 $prefix . '_width',
247 $prefix . '_color',
248 ];
249
250 foreach ( self::RESPONSIVE_SUFFIXES as $suffix ) {
251 $keys[] = $prefix . '_width' . $suffix;
252 }
253
254 return $keys;
255 }
256
257 private static function wrap_with_selector( string $css ): string {
258 if ( 1 === preg_match( '/^\s*[^{}]+\{\s*[\s\S]*\}\s*$/', $css ) ) {
259 return $css;
260 }
261
262 return 'selector { ' . rtrim( $css, "; \t\n\r" ) . '; }';
263 }
264
265 private static function split_css_classes( string $value ): array {
266 if ( '' === trim( $value ) ) {
267 return [];
268 }
269
270 return array_values( array_filter(
271 preg_split( '/\s+/', trim( $value ) ),
272 static fn( $token ) => '' !== $token
273 ) );
274 }
275 }
276