PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0
Elementor Website Builder – more than just a page builder v4.3.0
4.3.2 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 All 455 releases
elementor / modules / mcp / abilities / appliers / style-applier.php

style-applier.php in Elementor Website Builder – more than just a page builder 4.3.0, at modules/mcp/abilities/appliers/style-applier.php

325 lines 10.0 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\AtomicWidgets\CssConverter\Css_Converter;
6 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Style_Mapper_Factory;
7 use Elementor\Modules\Mcp\Abilities\Utils\Bulk_Operations_Result;
8 use Elementor\Modules\Mcp\Abilities\Utils\Style_Variants_Merger;
9 use Elementor\Modules\Mcp\Abilities\Utils\Widget_Context_Helper;
10 use Elementor\Modules\Variables\Utils\Variable_Type_Keys;
11 use Elementor\Plugin;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 class Style_Applier {
18
19 const LOCAL_STYLE_ID_PREFIX = 'e-';
20 const DESKTOP_BREAKPOINT = 'desktop';
21 const LOCAL_STYLE_LABEL = 'local';
22 const LOCAL_STYLE_TYPE = 'class';
23
24 private Css_Converter $css_converter;
25 private array $active_breakpoints;
26
27 public function __construct( Css_Converter $css_converter, array $active_breakpoints = [] ) {
28 $this->css_converter = $css_converter;
29 $this->active_breakpoints = $active_breakpoints;
30 }
31
32 /**
33 * @param array<string, array&> $config_id_index Index of subtree refs.
34 * @param array<string, string> $styles Per-config-id CSS strings.
35 * @param string $style_apply_mode `patch` or `replace`.
36 * @param array<string, array> $widget_configs Optional widget_type => config map (used for V3 mapping).
37 * @return array{error: \WP_Error|null, warnings: string[], variable_connections: array<string, array<string, string>>}
38 */
39 public function apply( array $config_id_index, array $styles, string $style_apply_mode = 'patch', array $widget_configs = [] ): array {
40 if ( empty( $styles ) ) {
41 return [
42 'error' => null,
43 'warnings' => [],
44 'warning_codes' => [],
45 'variable_connections' => [],
46 ];
47 }
48
49 $active_breakpoints = $this->get_active_breakpoints();
50 $errors = [];
51 $warnings = [];
52 $warning_codes = [];
53 $variable_connections = [];
54
55 foreach ( $styles as $config_id => $css_string ) {
56 if ( ! is_string( $css_string ) ) {
57 $errors[] = sprintf( '[%s] style must be a CSS string, got %s.', $config_id, gettype( $css_string ) );
58 continue;
59 }
60
61 if ( ! isset( $config_id_index[ $config_id ] ) ) {
62 continue;
63 }
64
65 $node = &$config_id_index[ $config_id ];
66
67 if ( V3_Node_Bridge::is_v3_node( $node ) ) {
68 $v3_result = $this->apply_v3_style( $node, $css_string, $style_apply_mode, $widget_configs );
69 foreach ( $v3_result['warnings'] as $warning ) {
70 $warnings[] = sprintf( '[%s] %s', $config_id, $warning );
71 }
72 $warning_codes = array_merge( $warning_codes, $v3_result['codes'] );
73 unset( $node );
74 continue;
75 }
76
77 $is_empty_css = '' === trim( $css_string );
78
79 if ( $is_empty_css ) {
80 if ( 'replace' === $style_apply_mode ) {
81 $existing_style_id = $this->find_existing_local_style_id( $node );
82 if ( $existing_style_id ) {
83 $node['styles'][ $existing_style_id ]['variants'] = [];
84 }
85 }
86 unset( $node );
87 continue;
88 }
89
90 $parse_results = new Bulk_Operations_Result();
91 $parsed = Style_Variants_Merger::parse_css_string(
92 $css_string,
93 $active_breakpoints,
94 0,
95 'update',
96 $parse_results,
97 fn() => $this->css_converter
98 );
99
100 if ( null === $parsed ) {
101 $result_data = $parse_results->to_array();
102 $errors[] = sprintf( '[%s] %s', $config_id, $result_data['results'][0]['message'] ?? 'CSS parse error.' );
103 unset( $node );
104 continue;
105 }
106
107 $new_variants = Style_Variants_Merger::build_variants( $parsed['breakpoint_blocks'], $this->css_converter );
108 $affected_bps = array_column( $parsed['breakpoint_blocks'], 'breakpoint' );
109 $removal_bps = $parsed['removal_breakpoints'];
110 $existing_style_id = $this->find_existing_local_style_id( $node );
111 $existing_variants = $node['styles'][ $existing_style_id ]['variants'] ?? [];
112 $existing_after_removal = array_values(
113 array_filter( $existing_variants, fn( $v ) => ! in_array( $v['meta']['breakpoint'] ?? null, $removal_bps, true ) )
114 );
115
116 $merged_variants = Style_Variants_Merger::apply_mode(
117 $existing_after_removal,
118 $new_variants,
119 $style_apply_mode,
120 $affected_bps
121 );
122
123 $variable_connections[ $config_id ] = $this->collect_variable_connections( $new_variants );
124
125 $this->write_variants_to_node( $node, $merged_variants, $existing_style_id );
126 unset( $node );
127 }
128
129 return [
130 'error' => $errors ? new \WP_Error(
131 'elementor_invalid_styles',
132 implode( ' ', $errors ),
133 [ 'status' => \WP_Http::BAD_REQUEST ]
134 ) : null,
135 'warnings' => $warnings,
136 'warning_codes' => array_values( array_unique( $warning_codes ) ),
137 'variable_connections' => $variable_connections,
138 ];
139 }
140
141 /**
142 * @param array $node
143 * @param string $css_string
144 * @param string $style_apply_mode
145 * @param array<string, array> $widget_configs
146 * @return array{warnings: string[], codes: string[]} Warnings (without config-id prefix) and stable codes.
147 */
148 private function apply_v3_style( array &$node, string $css_string, string $style_apply_mode = 'patch', array $widget_configs = [] ): array {
149 $warnings = [];
150 $codes = [];
151 $widget_type = $node['widgetType'] ?? '';
152 $widget_config = [];
153
154 if ( is_string( $widget_type ) && '' !== $widget_type ) {
155 $widget_config = $widget_configs[ $widget_type ]
156 ?? Widget_Context_Helper::get_widget_config( $widget_type )
157 ?? [];
158 }
159
160 $is_empty_css = '' === trim( $css_string );
161 $is_replace = 'replace' === $style_apply_mode;
162
163 if ( $is_replace ) {
164 V3_Node_Bridge::clear_style_settings( $node, (string) $widget_type, $widget_config );
165 }
166
167 if ( $is_empty_css ) {
168 return [
169 'warnings' => $warnings,
170 'codes' => $codes,
171 ];
172 }
173
174 $mapper = V3_Style_Mapper_Factory::create( $this->css_converter, $this->get_active_breakpoints() );
175 $result = $mapper->apply( $css_string, (string) $widget_type, $widget_config );
176
177 foreach ( $result['warnings'] as $warning ) {
178 $warnings[] = $warning;
179 }
180
181 if ( ! empty( $result['settings_patch'] ) ) {
182 $node['settings'] = array_merge( $node['settings'] ?? [], $result['settings_patch'] );
183 }
184
185 $unmapped = $result['unmapped_css'] ?? '';
186 $pro_warning = V3_Node_Bridge::apply_custom_css( $node, $unmapped, (string) $widget_type );
187 if ( null !== $pro_warning ) {
188 $warnings[] = $pro_warning;
189 $codes[] = 'v3_style_needs_pro';
190 }
191
192 if ( '' !== trim( $unmapped ) ) {
193 $snippet = self::truncate_css_snippet( $unmapped );
194 if ( null !== $pro_warning ) {
195 $warnings[] = sprintf(
196 /* translators: %s: CSS snippet that could not be mapped */
197 __( 'Some CSS could not be mapped to V3 settings and was dropped: %s', 'elementor' ),
198 $snippet
199 );
200 $codes[] = 'css_dropped';
201 } else {
202 $warnings[] = sprintf(
203 /* translators: %s: CSS snippet that could not be mapped */
204 __( 'Some CSS could not be mapped to V3 settings and was written to custom_css: %s', 'elementor' ),
205 $snippet
206 );
207 $codes[] = 'css_fallback_custom_css';
208 }
209 }
210
211 return [
212 'warnings' => $warnings,
213 'codes' => $codes,
214 ];
215 }
216
217 private static function truncate_css_snippet( string $css, int $max_length = 200 ): string {
218 $css = trim( preg_replace( '/\s+/', ' ', $css ) ?? $css );
219 if ( strlen( $css ) <= $max_length ) {
220 return $css;
221 }
222
223 return substr( $css, 0, $max_length - 3 ) . '...';
224 }
225
226 private function get_active_breakpoints(): array {
227 if ( ! empty( $this->active_breakpoints ) ) {
228 return $this->active_breakpoints;
229 }
230 return array_keys( Plugin::$instance->breakpoints->get_active_breakpoints() );
231 }
232
233 private function write_variants_to_node( array &$node, array $merged_variants, ?string $existing_style_id ): void {
234 if ( $existing_style_id ) {
235 $node['styles'][ $existing_style_id ]['variants'] = $merged_variants;
236 return;
237 }
238
239 if ( empty( $merged_variants ) ) {
240 return;
241 }
242
243 $style_id = $this->generate_local_style_id();
244 $node['styles'] = $node['styles'] ?? [];
245 $node['styles'][ $style_id ] = [
246 'id' => $style_id,
247 'label' => self::LOCAL_STYLE_LABEL,
248 'type' => self::LOCAL_STYLE_TYPE,
249 'variants' => $merged_variants,
250 ];
251 $node['settings'] = $this->add_style_to_classes( $node['settings'] ?? [], $style_id );
252 }
253
254 private function find_existing_local_style_id( array $node ): ?string {
255 foreach ( $node['styles'] ?? [] as $style_id => $_style ) {
256 if ( str_starts_with( (string) $style_id, self::LOCAL_STYLE_ID_PREFIX ) ) {
257 return $style_id;
258 }
259 }
260 return null;
261 }
262
263 private function add_style_to_classes( array $settings, string $style_id ): array {
264 $existing = $settings['classes']['value'] ?? [];
265 if ( ! is_array( $existing ) ) {
266 $existing = [];
267 }
268 if ( ! in_array( $style_id, $existing, true ) ) {
269 $existing[] = $style_id;
270 }
271
272 $settings['classes'] = [
273 '$$type' => 'classes',
274 'value' => array_values( $existing ),
275 ];
276
277 return $settings;
278 }
279
280 private function generate_local_style_id(): string {
281 return self::LOCAL_STYLE_ID_PREFIX . strtolower( \Elementor\Utils::generate_random_string() ) . '-' . dechex( wp_rand( 0x1000, 0xffff ) );
282 }
283
284 /**
285 * @param array[] $variants
286 * @return array<int, array{variable_id: string, var_type: string, control_path: string}>
287 */
288 private function collect_variable_connections( array $variants ): array {
289 $connections = [];
290
291 foreach ( $variants as $variant ) {
292 foreach ( $variant['props'] ?? [] as $prop_key => $prop_value ) {
293 if ( ! is_array( $prop_value ) ) {
294 continue;
295 }
296
297 $type = $prop_value['$$type'] ?? '';
298
299 if ( ! Variable_Type_Keys::is_variable_type( $type ) ) {
300 continue;
301 }
302
303 $connections[] = [
304 'variable_id' => (string) ( $prop_value['value'] ?? '' ),
305 'var_type' => $this->resolve_var_type_label( $type ),
306 'control_path' => (string) $prop_key,
307 ];
308 }
309 }
310
311 return $connections;
312 }
313
314 private function resolve_var_type_label( string $type ): string {
315 $labels = [
316 'global-color-variable' => 'color',
317 'global-font-variable' => 'font',
318 'global-size-variable' => 'size',
319 'global-custom-size-variable' => 'size',
320 ];
321
322 return $labels[ $type ] ?? $type;
323 }
324 }
325