PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.1.2
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.1.2
3.2.2 3.2.3 3.2.1 3.2.0 3.1.9 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.9 trunk 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.3 1.1.4 1.1.5 All 174 releases
master-addons / inc / admin / widget-builder / controls / class-control-base.php

class-control-base.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.1.2, at inc/admin/widget-builder/controls/class-control-base.php

903 lines 32.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace MasterAddons\Inc\Admin\WidgetBuilder\Controls;
3
4 defined('ABSPATH') || exit;
5
6 /**
7 * Base Control Builder
8 * Base class for all control type builders
9 */
10 abstract class Control_Base {
11
12 /**
13 * Build control output
14 *
15 * @param string $control_key Unique control key
16 * @param array $field Field configuration
17 * @return string Generated control code
18 */
19 abstract public function build($control_key, $field);
20
21 /**
22 * Get control type name
23 *
24 * @return string
25 */
26 abstract public function get_type();
27
28 /**
29 * Format property value
30 *
31 * @param string $name Property name
32 * @param mixed $value Property value
33 * @param int $depth Indentation depth
34 * @return string
35 */
36 protected function format_property($name, $value, $depth = 4) {
37 $tabs = str_repeat("\t", $depth);
38
39 // Special handling for 'default' property with arrays
40 if ($name === 'default' && is_array($value)) {
41 return $this->format_default_array($value, $depth);
42 }
43
44 // Format based on value type
45 if (is_bool($value)) {
46 return "{$tabs}'{$name}' => " . ($value ? 'true' : 'false') . ",\n";
47 } elseif (is_numeric($value)) {
48 return "{$tabs}'{$name}' => {$value},\n";
49 } elseif (is_string($value)) {
50 // Special handling for default values - don't translate them
51 if ($name === 'default') {
52 if ($value === '') {
53 return "{$tabs}'default' => '',\n";
54 }
55 // Default values should not be translated
56 return "{$tabs}'default' => '" . esc_js($value) . "',\n";
57 }
58 // Use translation function for other string values (labels, placeholders, etc.)
59 return "{$tabs}'{$name}' => esc_html__('" . esc_js($value) . "', 'master-addons'),\n";
60 } elseif (is_array($value)) {
61 return $this->format_array_property($name, $value, $depth);
62 }
63 return '';
64 }
65
66 /**
67 * Format default array property
68 * Special handling for default values that must be arrays
69 *
70 * @param array $value Default array value
71 * @param int $depth Indentation depth
72 * @return string
73 */
74 protected function format_default_array($value, $depth = 4) {
75 $tabs = str_repeat("\t", $depth);
76
77 // If empty array, return empty array
78 if (empty($value)) {
79 return "{$tabs}'default' => [],\n";
80 }
81
82 // Check if this is a simple indexed array (like for Select2 multiple)
83 // Example: ['value-1', 'value-2']
84 if (array_keys($value) === range(0, count($value) - 1)) {
85 $content = "{$tabs}'default' => [";
86 $items = [];
87 foreach ($value as $item) {
88 if (is_string($item)) {
89 $items[] = "'" . esc_js($item) . "'";
90 } elseif (is_numeric($item)) {
91 $items[] = $item;
92 } elseif (is_bool($item)) {
93 $items[] = $item ? 'true' : 'false';
94 } else {
95 $items[] = "'" . esc_js($item) . "'";
96 }
97 }
98 $content .= implode(', ', $items);
99 $content .= "],\n";
100 return $content;
101 }
102
103 // Handle associative arrays (with string keys)
104 $content = "{$tabs}'default' => [\n";
105
106 foreach ($value as $key => $val) {
107 if (is_string($key)) {
108 if (is_bool($val)) {
109 $content .= $tabs . "\t'" . esc_js($key) . "' => " . ($val ? 'true' : 'false') . ",\n";
110 } elseif (is_numeric($val)) {
111 $content .= $tabs . "\t'" . esc_js($key) . "' => " . $val . ",\n";
112 } elseif (is_array($val)) {
113 // Handle nested arrays
114 $content .= $this->format_nested_array($key, $val, $depth + 1);
115 } else {
116 // String values - don't use esc_html__ for defaults
117 $content .= $tabs . "\t'" . esc_js($key) . "' => '" . esc_js($val) . "',\n";
118 }
119 }
120 }
121
122 $content .= "{$tabs}],\n";
123 return $content;
124 }
125
126 /**
127 * Format array property
128 *
129 * @param string $name Property name
130 * @param array $array Array values
131 * @param int $depth Indentation depth
132 * @return string
133 */
134 protected function format_array_property($name, $array, $depth = 4) {
135 if (!is_array($array) || empty($array)) {
136 return '';
137 }
138
139 $tabs = str_repeat("\t", $depth);
140
141 // Check if simple array (indexed with string values)
142 if (array_keys($array) === range(0, count($array) - 1)) {
143 $content = "{$tabs}'{$name}' => [";
144 $items = [];
145 foreach ($array as $item) {
146 if (is_string($item)) {
147 $items[] = "'" . esc_js($item) . "'";
148 } else {
149 $items[] = $item;
150 }
151 }
152 $content .= implode(', ', $items);
153 $content .= "],\n";
154 return $content;
155 }
156
157 // Nested associative array
158 return $this->format_nested_array($name, $array, $depth);
159 }
160
161 /**
162 * Format nested array
163 *
164 * @param string $name Property name
165 * @param array $array Nested array
166 * @param int $depth Indentation depth
167 * @return string
168 */
169 protected function format_nested_array($name, $array, $depth = 4) {
170 if (!is_array($array) || empty($array)) {
171 return '';
172 }
173
174 $tabs = str_repeat("\t", $depth);
175 $content = "{$tabs}'{$name}' => [\n";
176
177 foreach ($array as $key => $value) {
178 if (is_array($value)) {
179 $content .= $this->format_nested_array($key, $value, $depth + 1);
180 } elseif (is_bool($value)) {
181 $content .= $tabs . "\t'" . esc_js($key) . "' => " . ($value ? 'true' : 'false') . ",\n";
182 } elseif (is_numeric($value)) {
183 $content .= $tabs . "\t'" . esc_js($key) . "' => {$value},\n";
184 } else {
185 $content .= $tabs . "\t'" . esc_js($key) . "' => '" . esc_js($value) . "',\n";
186 }
187 }
188
189 $content .= "{$tabs}],\n";
190 return $content;
191 }
192
193 /**
194 * Format localized string property
195 *
196 * @param string $name Property name
197 * @param string $value String value
198 * @param int $depth Indentation depth
199 * @return string
200 */
201 protected function format_string_property($name, $value, $depth = 4) {
202 $tabs = str_repeat("\t", $depth);
203 return "{$tabs}'{$name}' => esc_html__('" . esc_js($value) . "', 'master-addons'),\n";
204 }
205
206 /**
207 * Format plain string property (not localized)
208 *
209 * @param string $name Property name
210 * @param string $value String value
211 * @param int $depth Indentation depth
212 * @return string
213 */
214 protected function format_plain_string_property($name, $value, $depth = 4) {
215 $tabs = str_repeat("\t", $depth);
216 return "{$tabs}'{$name}' => '" . esc_js($value) . "',\n";
217 }
218
219 /**
220 * Format boolean property
221 *
222 * @param string $name Property name
223 * @param bool $value Boolean value
224 * @param int $depth Indentation depth
225 * @return string
226 */
227 protected function format_bool_property($name, $value, $depth = 4) {
228 $tabs = str_repeat("\t", $depth);
229 return "{$tabs}'{$name}' => " . ($value ? 'true' : 'false') . ",\n";
230 }
231
232 /**
233 * Format number property
234 *
235 * @param string $name Property name
236 * @param number $value Numeric value
237 * @param int $depth Indentation depth
238 * @return string
239 */
240 protected function format_number_property($name, $value, $depth = 4) {
241 $tabs = str_repeat("\t", $depth);
242 return "{$tabs}'{$name}' => {$value},\n";
243 }
244
245 /**
246 * Build common control properties
247 *
248 * @param array $field Field configuration
249 * @return string
250 */
251 protected function build_common_properties($field) {
252 $content = '';
253 // Default value
254 if (isset($field['default']) ) {
255 $content .= $this->format_property('default', $field['default']);
256 }
257
258 // Placeholder
259 if (!empty($field['placeholder'])) {
260 $content .= $this->format_string_property('placeholder', $field['placeholder']);
261 }
262
263 // Description
264 if (!empty($field['description'])) {
265 $content .= $this->format_string_property('description', $field['description']);
266 }
267
268 // Label block
269 if (isset($field['label_block'])) {
270 $content .= $this->format_bool_property('label_block', $field['label_block']);
271 }
272
273 // Show label
274 if (isset($field['show_label'])) {
275 $content .= $this->format_bool_property('show_label', $field['show_label']);
276 }
277
278 // Responsive is handled in build_control_header via method name change
279 // We don't output 'responsive' => true as a property
280
281 // Dynamic (dynamic content support)
282 if (isset($field['dynamic']) && $field['dynamic']) {
283 // Dynamic property should be an array with 'active' => true
284 $content .= "\t\t\t\t'dynamic' => [\n";
285 $content .= "\t\t\t\t\t'active' => true,\n";
286 $content .= "\t\t\t\t],\n";
287 }
288
289 // Frontend Available (expose control value to frontend JavaScript)
290 if (isset($field['frontend_available']) && $field['frontend_available']) {
291 $content .= $this->format_bool_property('frontend_available', true);
292 }
293
294 // Separator
295 if (!empty($field['separator']) && $field['separator'] !== 'default') {
296 $content .= $this->format_plain_string_property('separator', $field['separator']);
297 }
298
299 // Conditions - Convert from React UI format to Elementor format
300 // React sends: conditions: [{control_name: 'x', equality: 'equal', control_value: 'y'}]
301 // Elementor expects: 'condition' => ['x' => 'y'] OR 'conditions' => [...]
302 if (!empty($field['conditions']) && is_array($field['conditions'])) {
303 $content .= $this->build_conditions($field['conditions'], $field);
304 }
305
306 if (!empty($field['selectors']) && is_array($field['selectors'])) {
307 $content .= $this->build_selectors($field['selectors']);
308 }
309 elseif (!empty($field['selector']) && is_string($field['selector'])) {
310 $selector_value = $field['selector'];
311
312 if (strpos($selector_value, ',') !== false) {
313 $selector_parts = explode(',', $selector_value);
314 $processed_parts = [];
315
316 foreach ($selector_parts as $part) {
317 $part = trim($part);
318 if (!empty($part) && strpos($part, '{{WRAPPER}}') === false) {
319 $part = '{{WRAPPER}} ' . $part;
320 }
321 $processed_parts[] = $part;
322 }
323
324 $selector_value = implode(', ', $processed_parts);
325 } else {
326 // Single selector - ensure it has {{WRAPPER}}
327 if (strpos($selector_value, '{{WRAPPER}}') === false) {
328 $selector_value = '{{WRAPPER}} ' . $selector_value;
329 }
330 }
331
332 // Determine default CSS property based on control type
333 $default_property = 'color: {{VALUE}};'; // Default for most controls
334
335 if (isset($field['type'])) {
336 $type = strtolower($field['type']);
337 if (in_array($type, ['slider', 'number'])) {
338 $default_property = '{{VALUE}}';
339 }
340 elseif ($type === 'dimensions') {
341 return $content;
342 }
343 }
344
345 $content .= "\t\t\t\t'selectors' => [\n";
346 $content .= "\t\t\t\t\t'" . esc_js($selector_value) . "' => '" . esc_js($default_property) . "',\n";
347 $content .= "\t\t\t\t],\n";
348 }
349
350 return $content;
351 }
352
353 /**
354 * Build conditions from React UI format to Elementor format
355 *
356 * React format: [{control_name: 'field', equality: 'equal', control_value: 'value'}]
357 * Elementor formats:
358 * - Simple: 'condition' => ['field' => 'value'] (single condition with 'equal' operator)
359 * - Advanced: 'conditions' => ['terms' => [...]] (multiple conditions or non-equal operators)
360 *
361 * @param array $conditions_array Conditions array from React UI
362 * @param array $field Field context with tab and widget_id
363 * @return string
364 */
365 protected function build_conditions($conditions_array, $field = []) {
366 if (empty($conditions_array) || !is_array($conditions_array)) {
367 return '';
368 }
369
370 // Check if we need advanced format (multiple conditions OR non-equal operators)
371 $use_advanced_format = false;
372 $has_multiple_conditions = count($conditions_array) > 1;
373 $has_non_equal_operators = false;
374
375 foreach ($conditions_array as $condition) {
376 if (!empty($condition['equality']) && $condition['equality'] !== 'equal') {
377 $has_non_equal_operators = true;
378 break;
379 }
380 }
381
382 $use_advanced_format = $has_multiple_conditions || $has_non_equal_operators;
383
384 if ($use_advanced_format) {
385 // Use advanced 'conditions' format with 'terms'
386 return $this->build_advanced_conditions($conditions_array, $field);
387 } else {
388 // Use simple 'condition' format (single condition with equal operator)
389 $condition = $conditions_array[0];
390 if (!empty($condition['control_name']) && isset($condition['control_value'])) {
391 // Convert control_name to full key (only if it's a control reference)
392 $control_name = $this->convert_to_control_key($condition['control_name'], $field);
393
394 // Keep control_value as literal string (don't convert to control key)
395 // Users enter literal values like 'yes', 'no', 'value-1', etc.
396 $control_value = $condition['control_value'];
397
398 $simple_condition = [
399 $control_name => $control_value
400 ];
401 return $this->format_nested_array('condition', $simple_condition);
402 }
403 }
404
405 return '';
406 }
407
408 /**
409 * Build advanced conditions format with operators
410 *
411 * @param array $conditions_array Conditions array from React UI
412 * @param array $field Field context with tab and widget_id
413 * @return string
414 */
415 protected function build_advanced_conditions($conditions_array, $field = []) {
416 $terms = [];
417
418 // Map React equality to Elementor operators
419 $operator_map = [
420 'equal' => '===',
421 'not_equal' => '!==',
422 'in' => 'in',
423 'not_in' => '!in',
424 'contains' => 'contains',
425 'not_contains' => '!contains',
426 ];
427
428 foreach ($conditions_array as $condition) {
429 if (!empty($condition['control_name']) && isset($condition['control_value'])) {
430 $equality = $condition['equality'] ?? 'equal';
431 $operator = $operator_map[$equality] ?? '===';
432
433 // Convert control_name to full key
434 $control_name = $this->convert_to_control_key($condition['control_name'], $field);
435
436 // Keep control_value as literal string (don't convert)
437 // Users enter literal values like 'yes', 'no', 'value-1', etc.
438 $control_value = $condition['control_value'];
439
440 $terms[] = [
441 'name' => $control_name,
442 'operator' => $operator,
443 'value' => $control_value
444 ];
445 }
446 }
447
448 if (empty($terms)) {
449 return '';
450 }
451
452 // Build the conditions array with terms
453 $content = "\t\t\t\t'conditions' => [\n";
454 $content .= "\t\t\t\t\t'terms' => [\n";
455
456 foreach ($terms as $term) {
457 $content .= "\t\t\t\t\t\t[\n";
458 $content .= "\t\t\t\t\t\t\t'name' => '" . esc_js($term['name']) . "',\n";
459 $content .= "\t\t\t\t\t\t\t'operator' => '" . esc_js($term['operator']) . "',\n";
460 $content .= "\t\t\t\t\t\t\t'value' => '" . esc_js($term['value']) . "',\n";
461 $content .= "\t\t\t\t\t\t],\n";
462 }
463
464 $content .= "\t\t\t\t\t],\n";
465 $content .= "\t\t\t\t],\n";
466
467 return $content;
468 }
469
470 /**
471 * Sanitize label to create control/section key
472 * Converts spaces to underscores instead of hyphens
473 *
474 * @param string $label Label to sanitize
475 * @return string Sanitized key with underscores
476 */
477 protected function sanitize_key($label) {
478 // Convert to lowercase
479 $key = strtolower($label);
480
481 // Replace spaces with underscores
482 $key = str_replace(' ', '_', $key);
483
484 // Remove special characters, keeping only alphanumeric and underscores
485 $key = preg_replace('/[^a-z0-9_]/', '', $key);
486
487 // Remove multiple consecutive underscores
488 $key = preg_replace('/_+/', '_', $key);
489
490 // Trim underscores from beginning and end
491 $key = trim($key, '_');
492
493 return $key;
494 }
495
496 /**
497 * Convert short control name to full control key
498 *
499 * @param string $name Short name like 'text' or already full key like 'jltma_content_text_7641'
500 * @param array $field Field context with _tab, _widget_id, _tab_prefix
501 * @return string Full control key
502 */
503 protected function convert_to_control_key($name, $field = []) {
504 $widget_id = $field['_widget_id'] ?? '';
505 $widget_id_suffix = '_' . $widget_id;
506
507 // If already a full key (ends with _widget_id), return as-is
508 if (!empty($widget_id) && substr($name, -strlen($widget_id_suffix)) === $widget_id_suffix) {
509 return $name;
510 }
511
512 // If name already has a jltma_ prefix (but no widget_id suffix), just append widget_id
513 if (strpos($name, 'jltma_content_') === 0 || strpos($name, 'jltma_style_') === 0 || strpos($name, 'jltma_advanced_') === 0) {
514 return $name . $widget_id_suffix;
515 }
516
517 // Search across ALL sections to find which tab the target control actually lives in
518 $sanitized_name = $this->sanitize_key($name);
519 if (!empty($field['_sections_data']) && is_array($field['_sections_data'])) {
520 $tab_prefix_map = [
521 'content' => 'jltma_content_',
522 'style' => 'jltma_style_',
523 'advanced' => 'jltma_advanced_',
524 ];
525
526 foreach ($field['_sections_data'] as $section) {
527 if (!is_array($section)) {
528 continue;
529 }
530 $controls = !empty($section['controls']) ? $section['controls'] : (!empty($section['fields']) ? $section['fields'] : []);
531 foreach ($controls as $control) {
532 if (!empty($control['name']) && $this->sanitize_key($control['name']) === $sanitized_name) {
533 $tab = !empty($section['tab']) ? $section['tab'] : 'content';
534 $prefix = $tab_prefix_map[$tab] ?? 'jltma_content_';
535 return $prefix . $sanitized_name . $widget_id_suffix;
536 }
537 }
538 }
539 }
540
541 // Fallback: use current field's tab prefix
542 $tab_prefix = $field['_tab_prefix'] ?? 'jltma_content_';
543 return $tab_prefix . $sanitized_name . $widget_id_suffix;
544 }
545
546 /**
547 * Build selectors from React UI format to Elementor format
548 *
549 * React format: [{selector: '{{WRAPPER}} .class', properties: 'color: {{VALUE}};'}]
550 * Elementor format: 'selectors' => ['{{WRAPPER}} .class' => 'color: {{VALUE}};']
551 *
552 * @param array $selectors_array Selectors array from React UI
553 * @return string
554 */
555 protected function build_selectors($selectors_array) {
556 if (empty($selectors_array) || !is_array($selectors_array)) {
557 return '';
558 }
559
560 // Convert React selectors format to Elementor format
561 $elementor_selectors = [];
562
563 foreach ($selectors_array as $item) {
564 if (!empty($item['selector']) && !empty($item['properties'])) {
565 $selector = $item['selector'];
566
567 // Process comma-separated selectors
568 // Each individual selector needs {{WRAPPER}}
569 if (strpos($selector, ',') !== false) {
570 $selector_parts = explode(',', $selector);
571 $processed_parts = [];
572
573 foreach ($selector_parts as $part) {
574 $part = trim($part);
575 // Add {{WRAPPER}} if not already present
576 if (!empty($part) && strpos($part, '{{WRAPPER}}') === false) {
577 $part = '{{WRAPPER}} ' . $part;
578 }
579 $processed_parts[] = $part;
580 }
581
582 $selector = implode(', ', $processed_parts);
583 } else {
584 // Single selector - ensure it has {{WRAPPER}}
585 if (strpos($selector, '{{WRAPPER}}') === false) {
586 $selector = '{{WRAPPER}} ' . $selector;
587 }
588 }
589
590 $elementor_selectors[$selector] = $item['properties'];
591 }
592 }
593
594 if (empty($elementor_selectors)) {
595 return '';
596 }
597
598 $content = "\t\t\t\t'selectors' => [\n";
599 foreach ($elementor_selectors as $selector => $css) {
600 $content .= "\t\t\t\t\t'" . esc_js($selector) . "' => '" . esc_js($css) . "',\n";
601 }
602 $content .= "\t\t\t\t],\n";
603
604 return $content;
605 }
606
607 /**
608 * Build control header
609 *
610 * @param string $control_key Unique control key
611 * @param string $label Control label
612 * @param string $type Control type
613 * @param bool $is_responsive Whether this is a responsive control
614 * @return string
615 */
616 protected function build_control_header($control_key, $label, $type, $is_responsive = false) {
617 // Use add_responsive_control if responsive is enabled
618 $method = $is_responsive ? 'add_responsive_control' : 'add_control';
619
620 $content = "\t\t\$this->{$method}(\n";
621 $content .= "\t\t\t'{$control_key}',\n";
622 $content .= "\t\t\t[\n";
623 $content .= "\t\t\t\t'label' => esc_html__('" . esc_js($label) . "', 'master-addons'),\n";
624 $content .= "\t\t\t\t'type' => Controls_Manager::{$type},\n";
625 return $content;
626 }
627
628 /**
629 * Build control footer
630 *
631 * @return string
632 */
633 protected function build_control_footer() {
634 return "\t\t\t]\n\t\t);\n\n";
635 }
636
637 /* ---------------------------------------------------------------------
638 * Runtime config layer
639 *
640 * Parallel to build() / build_common_properties(), but returns a plain
641 * config ARRAY instead of generated PHP source. Consumed by Dynamic_Widget
642 * at register_controls() time so no PHP file is ever written or executed.
643 * ------------------------------------------------------------------- */
644
645 /**
646 * Default control label when the field has none. Overridden per control.
647 *
648 * @return string
649 */
650 protected function default_label() {
651 return 'Control';
652 }
653
654 /**
655 * Resolve the Elementor Controls_Manager constant value for this control type.
656 *
657 * @return string
658 */
659 protected function type_constant() {
660 $const = '\\Elementor\\Controls_Manager::' . $this->get_type();
661 return defined($const) ? constant($const) : 'text';
662 }
663
664 /**
665 * Control-type-specific args (input_type, rows, options, ...). Overridden per control.
666 *
667 * @param array $field
668 * @return array
669 */
670 protected function get_type_specific_config($field) {
671 return [];
672 }
673
674 /**
675 * Build the control config array (runtime equivalent of build()).
676 *
677 * @param string $control_key Unique control key.
678 * @param array $field Field configuration from stored schema.
679 * @return array ['key' => string, 'responsive' => bool, 'args' => array]
680 */
681 public function get_config($control_key, $field) {
682 $label = !empty($field['label']) ? $field['label'] : $this->default_label();
683
684 $args = [
685 // translators: dynamic user-defined control label.
686 'label' => esc_html($label),
687 'type' => $this->type_constant(),
688 ];
689
690 $args = array_merge($args, $this->get_type_specific_config($field));
691 $args = array_merge($args, $this->get_common_config($field));
692
693 return [
694 'key' => $control_key,
695 'responsive' => !empty($field['responsive']),
696 'args' => $args,
697 ];
698 }
699
700 /**
701 * Common control args (default, placeholder, description, label_block,
702 * show_label, dynamic, frontend_available, separator, condition(s), selectors).
703 * Array equivalent of build_common_properties().
704 *
705 * @param array $field
706 * @return array
707 */
708 protected function get_common_config($field) {
709 $config = [];
710
711 if (isset($field['default'])) {
712 $config['default'] = $field['default'];
713 }
714 if (!empty($field['placeholder'])) {
715 // translators: dynamic user-defined placeholder.
716 $config['placeholder'] = esc_html($field['placeholder']);
717 }
718 if (!empty($field['description'])) {
719 // translators: dynamic user-defined description.
720 $config['description'] = esc_html($field['description']);
721 }
722 if (isset($field['label_block'])) {
723 $config['label_block'] = (bool) $field['label_block'];
724 }
725 if (isset($field['show_label'])) {
726 $config['show_label'] = (bool) $field['show_label'];
727 }
728 if (!empty($field['dynamic'])) {
729 $config['dynamic'] = ['active' => true];
730 }
731 if (!empty($field['frontend_available'])) {
732 $config['frontend_available'] = true;
733 }
734 if (!empty($field['separator']) && $field['separator'] !== 'default') {
735 $config['separator'] = $field['separator'];
736 }
737 if (!empty($field['conditions']) && is_array($field['conditions'])) {
738 $config = array_merge($config, $this->get_conditions_config($field['conditions'], $field));
739 }
740 if (!empty($field['selectors']) && is_array($field['selectors'])) {
741 $config = array_merge($config, $this->get_selectors_config($field['selectors']));
742 } elseif (!empty($field['selector']) && is_string($field['selector'])) {
743 $config = array_merge($config, $this->get_single_selector_config($field));
744 }
745
746 return $config;
747 }
748
749 /**
750 * Conditions config (array equivalent of build_conditions/build_advanced_conditions).
751 *
752 * @param array $conditions_array
753 * @param array $field
754 * @return array
755 */
756 protected function get_conditions_config($conditions_array, $field = []) {
757 if (empty($conditions_array) || !is_array($conditions_array)) {
758 return [];
759 }
760
761 $has_multiple = count($conditions_array) > 1;
762 $has_non_equal = false;
763 foreach ($conditions_array as $condition) {
764 if (!empty($condition['equality']) && $condition['equality'] !== 'equal') {
765 $has_non_equal = true;
766 break;
767 }
768 }
769
770 if ($has_multiple || $has_non_equal) {
771 $operator_map = [
772 'equal' => '===',
773 'not_equal' => '!==',
774 'in' => 'in',
775 'not_in' => '!in',
776 'contains' => 'contains',
777 'not_contains' => '!contains',
778 ];
779 $terms = [];
780 foreach ($conditions_array as $condition) {
781 if (!empty($condition['control_name']) && isset($condition['control_value'])) {
782 $equality = $condition['equality'] ?? 'equal';
783 $terms[] = [
784 'name' => $this->convert_to_control_key($condition['control_name'], $field),
785 'operator' => $operator_map[$equality] ?? '===',
786 'value' => $condition['control_value'],
787 ];
788 }
789 }
790 return $terms ? ['conditions' => ['terms' => $terms]] : [];
791 }
792
793 $condition = $conditions_array[0];
794 if (!empty($condition['control_name']) && isset($condition['control_value'])) {
795 return [
796 'condition' => [
797 $this->convert_to_control_key($condition['control_name'], $field) => $condition['control_value'],
798 ],
799 ];
800 }
801
802 return [];
803 }
804
805 /**
806 * Selectors config from the React selectors array (array equivalent of build_selectors).
807 *
808 * @param array $selectors_array
809 * @return array
810 */
811 protected function get_selectors_config($selectors_array) {
812 if (empty($selectors_array) || !is_array($selectors_array)) {
813 return [];
814 }
815 $out = [];
816 foreach ($selectors_array as $item) {
817 if (!empty($item['selector']) && !empty($item['properties'])) {
818 $out[$this->normalize_selector($item['selector'])] = $item['properties'];
819 }
820 }
821 return $out ? ['selectors' => $out] : [];
822 }
823
824 /**
825 * Single-selector config (array equivalent of the inline selector handling
826 * in build_common_properties).
827 *
828 * @param array $field
829 * @return array
830 */
831 protected function get_single_selector_config($field) {
832 $default_property = 'color: {{VALUE}};';
833 if (isset($field['type'])) {
834 $type = strtolower($field['type']);
835 if (in_array($type, ['slider', 'number'], true)) {
836 $default_property = '{{VALUE}}';
837 } elseif ($type === 'dimensions') {
838 return [];
839 }
840 }
841 return ['selectors' => [$this->normalize_selector($field['selector']) => $default_property]];
842 }
843
844 /**
845 * Build a group-control descriptor consumed by Dynamic_Widget::jltma_apply_control().
846 * Group controls register via add_group_control() (no 'type', no responsive method).
847 *
848 * @param string $control_key Control key (kept for descriptor shape; group uses args['name']).
849 * @param string $group_class Fully-qualified Elementor Group_Control_* class.
850 * @param array $args add_group_control args.
851 * @return array
852 */
853 protected function group_descriptor($control_key, $group_class, $args) {
854 return [
855 'key' => $control_key,
856 'method' => 'add_group_control',
857 'group_type' => call_user_func([$group_class, 'get_type']),
858 'args' => $args,
859 ];
860 }
861
862 /**
863 * Partial common args used by Border/Box_Shadow group controls (show_label,
864 * separator, conditions only — they do NOT emit the full common set).
865 *
866 * @param array $field
867 * @return array
868 */
869 protected function get_group_partial_common($field) {
870 $common = [];
871 if (isset($field['show_label'])) {
872 $common['show_label'] = (bool) $field['show_label'];
873 }
874 if (!empty($field['separator']) && $field['separator'] !== 'default') {
875 $common['separator'] = $field['separator'];
876 }
877 if (!empty($field['conditions']) && is_array($field['conditions'])) {
878 $common = array_merge($common, $this->get_conditions_config($field['conditions'], $field));
879 }
880 return $common;
881 }
882
883 /**
884 * Ensure each selector part is scoped with {{WRAPPER}}.
885 *
886 * @param string $selector
887 * @return string
888 */
889 protected function normalize_selector($selector) {
890 if (strpos($selector, ',') !== false) {
891 $parts = array_map('trim', explode(',', $selector));
892 foreach ($parts as &$part) {
893 if ($part !== '' && strpos($part, '{{WRAPPER}}') === false) {
894 $part = '{{WRAPPER}} ' . $part;
895 }
896 }
897 unset($part);
898 return implode(', ', $parts);
899 }
900 return strpos($selector, '{{WRAPPER}}') === false ? '{{WRAPPER}} ' . $selector : $selector;
901 }
902 }
903