format_default_array($value, $depth); } // Format based on value type if (is_bool($value)) { return "{$tabs}'{$name}' => " . ($value ? 'true' : 'false') . ",\n"; } elseif (is_numeric($value)) { return "{$tabs}'{$name}' => {$value},\n"; } elseif (is_string($value)) { // Special handling for default values - don't translate them if ($name === 'default') { if ($value === '') { return "{$tabs}'default' => '',\n"; } // Default values should not be translated return "{$tabs}'default' => '" . esc_js($value) . "',\n"; } // Use translation function for other string values (labels, placeholders, etc.) return "{$tabs}'{$name}' => esc_html__('" . esc_js($value) . "', 'master-addons'),\n"; } elseif (is_array($value)) { return $this->format_array_property($name, $value, $depth); } return ''; } /** * Format default array property * Special handling for default values that must be arrays * * @param array $value Default array value * @param int $depth Indentation depth * @return string */ protected function format_default_array($value, $depth = 4) { $tabs = str_repeat("\t", $depth); // If empty array, return empty array if (empty($value)) { return "{$tabs}'default' => [],\n"; } // Check if this is a simple indexed array (like for Select2 multiple) // Example: ['value-1', 'value-2'] if (array_keys($value) === range(0, count($value) - 1)) { $content = "{$tabs}'default' => ["; $items = []; foreach ($value as $item) { if (is_string($item)) { $items[] = "'" . esc_js($item) . "'"; } elseif (is_numeric($item)) { $items[] = $item; } elseif (is_bool($item)) { $items[] = $item ? 'true' : 'false'; } else { $items[] = "'" . esc_js($item) . "'"; } } $content .= implode(', ', $items); $content .= "],\n"; return $content; } // Handle associative arrays (with string keys) $content = "{$tabs}'default' => [\n"; foreach ($value as $key => $val) { if (is_string($key)) { if (is_bool($val)) { $content .= $tabs . "\t'" . esc_js($key) . "' => " . ($val ? 'true' : 'false') . ",\n"; } elseif (is_numeric($val)) { $content .= $tabs . "\t'" . esc_js($key) . "' => " . $val . ",\n"; } elseif (is_array($val)) { // Handle nested arrays $content .= $this->format_nested_array($key, $val, $depth + 1); } else { // String values - don't use esc_html__ for defaults $content .= $tabs . "\t'" . esc_js($key) . "' => '" . esc_js($val) . "',\n"; } } } $content .= "{$tabs}],\n"; return $content; } /** * Format array property * * @param string $name Property name * @param array $array Array values * @param int $depth Indentation depth * @return string */ protected function format_array_property($name, $array, $depth = 4) { if (!is_array($array) || empty($array)) { return ''; } $tabs = str_repeat("\t", $depth); // Check if simple array (indexed with string values) if (array_keys($array) === range(0, count($array) - 1)) { $content = "{$tabs}'{$name}' => ["; $items = []; foreach ($array as $item) { if (is_string($item)) { $items[] = "'" . esc_js($item) . "'"; } else { $items[] = $item; } } $content .= implode(', ', $items); $content .= "],\n"; return $content; } // Nested associative array return $this->format_nested_array($name, $array, $depth); } /** * Format nested array * * @param string $name Property name * @param array $array Nested array * @param int $depth Indentation depth * @return string */ protected function format_nested_array($name, $array, $depth = 4) { if (!is_array($array) || empty($array)) { return ''; } $tabs = str_repeat("\t", $depth); $content = "{$tabs}'{$name}' => [\n"; foreach ($array as $key => $value) { if (is_array($value)) { $content .= $this->format_nested_array($key, $value, $depth + 1); } elseif (is_bool($value)) { $content .= $tabs . "\t'" . esc_js($key) . "' => " . ($value ? 'true' : 'false') . ",\n"; } elseif (is_numeric($value)) { $content .= $tabs . "\t'" . esc_js($key) . "' => {$value},\n"; } else { $content .= $tabs . "\t'" . esc_js($key) . "' => '" . esc_js($value) . "',\n"; } } $content .= "{$tabs}],\n"; return $content; } /** * Format localized string property * * @param string $name Property name * @param string $value String value * @param int $depth Indentation depth * @return string */ protected function format_string_property($name, $value, $depth = 4) { $tabs = str_repeat("\t", $depth); return "{$tabs}'{$name}' => esc_html__('" . esc_js($value) . "', 'master-addons'),\n"; } /** * Format plain string property (not localized) * * @param string $name Property name * @param string $value String value * @param int $depth Indentation depth * @return string */ protected function format_plain_string_property($name, $value, $depth = 4) { $tabs = str_repeat("\t", $depth); return "{$tabs}'{$name}' => '" . esc_js($value) . "',\n"; } /** * Format boolean property * * @param string $name Property name * @param bool $value Boolean value * @param int $depth Indentation depth * @return string */ protected function format_bool_property($name, $value, $depth = 4) { $tabs = str_repeat("\t", $depth); return "{$tabs}'{$name}' => " . ($value ? 'true' : 'false') . ",\n"; } /** * Format number property * * @param string $name Property name * @param number $value Numeric value * @param int $depth Indentation depth * @return string */ protected function format_number_property($name, $value, $depth = 4) { $tabs = str_repeat("\t", $depth); return "{$tabs}'{$name}' => {$value},\n"; } /** * Build common control properties * * @param array $field Field configuration * @return string */ protected function build_common_properties($field) { $content = ''; // Default value if (isset($field['default']) ) { $content .= $this->format_property('default', $field['default']); } // Placeholder if (!empty($field['placeholder'])) { $content .= $this->format_string_property('placeholder', $field['placeholder']); } // Description if (!empty($field['description'])) { $content .= $this->format_string_property('description', $field['description']); } // Label block if (isset($field['label_block'])) { $content .= $this->format_bool_property('label_block', $field['label_block']); } // Show label if (isset($field['show_label'])) { $content .= $this->format_bool_property('show_label', $field['show_label']); } // Responsive is handled in build_control_header via method name change // We don't output 'responsive' => true as a property // Dynamic (dynamic content support) if (isset($field['dynamic']) && $field['dynamic']) { // Dynamic property should be an array with 'active' => true $content .= "\t\t\t\t'dynamic' => [\n"; $content .= "\t\t\t\t\t'active' => true,\n"; $content .= "\t\t\t\t],\n"; } // Frontend Available (expose control value to frontend JavaScript) if (isset($field['frontend_available']) && $field['frontend_available']) { $content .= $this->format_bool_property('frontend_available', true); } // Separator if (!empty($field['separator']) && $field['separator'] !== 'default') { $content .= $this->format_plain_string_property('separator', $field['separator']); } // Conditions - Convert from React UI format to Elementor format // React sends: conditions: [{control_name: 'x', equality: 'equal', control_value: 'y'}] // Elementor expects: 'condition' => ['x' => 'y'] OR 'conditions' => [...] if (!empty($field['conditions']) && is_array($field['conditions'])) { $content .= $this->build_conditions($field['conditions'], $field); } if (!empty($field['selectors']) && is_array($field['selectors'])) { $content .= $this->build_selectors($field['selectors']); } elseif (!empty($field['selector']) && is_string($field['selector'])) { $selector_value = $field['selector']; if (strpos($selector_value, ',') !== false) { $selector_parts = explode(',', $selector_value); $processed_parts = []; foreach ($selector_parts as $part) { $part = trim($part); if (!empty($part) && strpos($part, '{{WRAPPER}}') === false) { $part = '{{WRAPPER}} ' . $part; } $processed_parts[] = $part; } $selector_value = implode(', ', $processed_parts); } else { // Single selector - ensure it has {{WRAPPER}} if (strpos($selector_value, '{{WRAPPER}}') === false) { $selector_value = '{{WRAPPER}} ' . $selector_value; } } // Determine default CSS property based on control type $default_property = 'color: {{VALUE}};'; // Default for most controls if (isset($field['type'])) { $type = strtolower($field['type']); if (in_array($type, ['slider', 'number'])) { $default_property = '{{VALUE}}'; } elseif ($type === 'dimensions') { return $content; } } $content .= "\t\t\t\t'selectors' => [\n"; $content .= "\t\t\t\t\t'" . esc_js($selector_value) . "' => '" . esc_js($default_property) . "',\n"; $content .= "\t\t\t\t],\n"; } return $content; } /** * Build conditions from React UI format to Elementor format * * React format: [{control_name: 'field', equality: 'equal', control_value: 'value'}] * Elementor formats: * - Simple: 'condition' => ['field' => 'value'] (single condition with 'equal' operator) * - Advanced: 'conditions' => ['terms' => [...]] (multiple conditions or non-equal operators) * * @param array $conditions_array Conditions array from React UI * @param array $field Field context with tab and widget_id * @return string */ protected function build_conditions($conditions_array, $field = []) { if (empty($conditions_array) || !is_array($conditions_array)) { return ''; } // Check if we need advanced format (multiple conditions OR non-equal operators) $use_advanced_format = false; $has_multiple_conditions = count($conditions_array) > 1; $has_non_equal_operators = false; foreach ($conditions_array as $condition) { if (!empty($condition['equality']) && $condition['equality'] !== 'equal') { $has_non_equal_operators = true; break; } } $use_advanced_format = $has_multiple_conditions || $has_non_equal_operators; if ($use_advanced_format) { // Use advanced 'conditions' format with 'terms' return $this->build_advanced_conditions($conditions_array, $field); } else { // Use simple 'condition' format (single condition with equal operator) $condition = $conditions_array[0]; if (!empty($condition['control_name']) && isset($condition['control_value'])) { // Convert control_name to full key (only if it's a control reference) $control_name = $this->convert_to_control_key($condition['control_name'], $field); // Keep control_value as literal string (don't convert to control key) // Users enter literal values like 'yes', 'no', 'value-1', etc. $control_value = $condition['control_value']; $simple_condition = [ $control_name => $control_value ]; return $this->format_nested_array('condition', $simple_condition); } } return ''; } /** * Build advanced conditions format with operators * * @param array $conditions_array Conditions array from React UI * @param array $field Field context with tab and widget_id * @return string */ protected function build_advanced_conditions($conditions_array, $field = []) { $terms = []; // Map React equality to Elementor operators $operator_map = [ 'equal' => '===', 'not_equal' => '!==', 'in' => 'in', 'not_in' => '!in', 'contains' => 'contains', 'not_contains' => '!contains', ]; foreach ($conditions_array as $condition) { if (!empty($condition['control_name']) && isset($condition['control_value'])) { $equality = $condition['equality'] ?? 'equal'; $operator = $operator_map[$equality] ?? '==='; // Convert control_name to full key $control_name = $this->convert_to_control_key($condition['control_name'], $field); // Keep control_value as literal string (don't convert) // Users enter literal values like 'yes', 'no', 'value-1', etc. $control_value = $condition['control_value']; $terms[] = [ 'name' => $control_name, 'operator' => $operator, 'value' => $control_value ]; } } if (empty($terms)) { return ''; } // Build the conditions array with terms $content = "\t\t\t\t'conditions' => [\n"; $content .= "\t\t\t\t\t'terms' => [\n"; foreach ($terms as $term) { $content .= "\t\t\t\t\t\t[\n"; $content .= "\t\t\t\t\t\t\t'name' => '" . esc_js($term['name']) . "',\n"; $content .= "\t\t\t\t\t\t\t'operator' => '" . esc_js($term['operator']) . "',\n"; $content .= "\t\t\t\t\t\t\t'value' => '" . esc_js($term['value']) . "',\n"; $content .= "\t\t\t\t\t\t],\n"; } $content .= "\t\t\t\t\t],\n"; $content .= "\t\t\t\t],\n"; return $content; } /** * Sanitize label to create control/section key * Converts spaces to underscores instead of hyphens * * @param string $label Label to sanitize * @return string Sanitized key with underscores */ protected function sanitize_key($label) { // Convert to lowercase $key = strtolower($label); // Replace spaces with underscores $key = str_replace(' ', '_', $key); // Remove special characters, keeping only alphanumeric and underscores $key = preg_replace('/[^a-z0-9_]/', '', $key); // Remove multiple consecutive underscores $key = preg_replace('/_+/', '_', $key); // Trim underscores from beginning and end $key = trim($key, '_'); return $key; } /** * Convert short control name to full control key * * @param string $name Short name like 'text' or already full key like 'jltma_content_text_7641' * @param array $field Field context with _tab, _widget_id, _tab_prefix * @return string Full control key */ protected function convert_to_control_key($name, $field = []) { $widget_id = $field['_widget_id'] ?? ''; $widget_id_suffix = '_' . $widget_id; // If already a full key (ends with _widget_id), return as-is if (!empty($widget_id) && substr($name, -strlen($widget_id_suffix)) === $widget_id_suffix) { return $name; } // If name already has a jltma_ prefix (but no widget_id suffix), just append widget_id if (strpos($name, 'jltma_content_') === 0 || strpos($name, 'jltma_style_') === 0 || strpos($name, 'jltma_advanced_') === 0) { return $name . $widget_id_suffix; } // Search across ALL sections to find which tab the target control actually lives in $sanitized_name = $this->sanitize_key($name); if (!empty($field['_sections_data']) && is_array($field['_sections_data'])) { $tab_prefix_map = [ 'content' => 'jltma_content_', 'style' => 'jltma_style_', 'advanced' => 'jltma_advanced_', ]; foreach ($field['_sections_data'] as $section) { if (!is_array($section)) { continue; } $controls = !empty($section['controls']) ? $section['controls'] : (!empty($section['fields']) ? $section['fields'] : []); foreach ($controls as $control) { if (!empty($control['name']) && $this->sanitize_key($control['name']) === $sanitized_name) { $tab = !empty($section['tab']) ? $section['tab'] : 'content'; $prefix = $tab_prefix_map[$tab] ?? 'jltma_content_'; return $prefix . $sanitized_name . $widget_id_suffix; } } } } // Fallback: use current field's tab prefix $tab_prefix = $field['_tab_prefix'] ?? 'jltma_content_'; return $tab_prefix . $sanitized_name . $widget_id_suffix; } /** * Build selectors from React UI format to Elementor format * * React format: [{selector: '{{WRAPPER}} .class', properties: 'color: {{VALUE}};'}] * Elementor format: 'selectors' => ['{{WRAPPER}} .class' => 'color: {{VALUE}};'] * * @param array $selectors_array Selectors array from React UI * @return string */ protected function build_selectors($selectors_array) { if (empty($selectors_array) || !is_array($selectors_array)) { return ''; } // Convert React selectors format to Elementor format $elementor_selectors = []; foreach ($selectors_array as $item) { if (!empty($item['selector']) && !empty($item['properties'])) { $selector = $item['selector']; // Process comma-separated selectors // Each individual selector needs {{WRAPPER}} if (strpos($selector, ',') !== false) { $selector_parts = explode(',', $selector); $processed_parts = []; foreach ($selector_parts as $part) { $part = trim($part); // Add {{WRAPPER}} if not already present if (!empty($part) && strpos($part, '{{WRAPPER}}') === false) { $part = '{{WRAPPER}} ' . $part; } $processed_parts[] = $part; } $selector = implode(', ', $processed_parts); } else { // Single selector - ensure it has {{WRAPPER}} if (strpos($selector, '{{WRAPPER}}') === false) { $selector = '{{WRAPPER}} ' . $selector; } } $elementor_selectors[$selector] = $item['properties']; } } if (empty($elementor_selectors)) { return ''; } $content = "\t\t\t\t'selectors' => [\n"; foreach ($elementor_selectors as $selector => $css) { $content .= "\t\t\t\t\t'" . esc_js($selector) . "' => '" . esc_js($css) . "',\n"; } $content .= "\t\t\t\t],\n"; return $content; } /** * Build control header * * @param string $control_key Unique control key * @param string $label Control label * @param string $type Control type * @param bool $is_responsive Whether this is a responsive control * @return string */ protected function build_control_header($control_key, $label, $type, $is_responsive = false) { // Use add_responsive_control if responsive is enabled $method = $is_responsive ? 'add_responsive_control' : 'add_control'; $content = "\t\t\$this->{$method}(\n"; $content .= "\t\t\t'{$control_key}',\n"; $content .= "\t\t\t[\n"; $content .= "\t\t\t\t'label' => esc_html__('" . esc_js($label) . "', 'master-addons'),\n"; $content .= "\t\t\t\t'type' => Controls_Manager::{$type},\n"; return $content; } /** * Build control footer * * @return string */ protected function build_control_footer() { return "\t\t\t]\n\t\t);\n\n"; } /* --------------------------------------------------------------------- * Runtime config layer * * Parallel to build() / build_common_properties(), but returns a plain * config ARRAY instead of generated PHP source. Consumed by Dynamic_Widget * at register_controls() time so no PHP file is ever written or executed. * ------------------------------------------------------------------- */ /** * Default control label when the field has none. Overridden per control. * * @return string */ protected function default_label() { return 'Control'; } /** * Resolve the Elementor Controls_Manager constant value for this control type. * * @return string */ protected function type_constant() { $const = '\\Elementor\\Controls_Manager::' . $this->get_type(); return defined($const) ? constant($const) : 'text'; } /** * Control-type-specific args (input_type, rows, options, ...). Overridden per control. * * @param array $field * @return array */ protected function get_type_specific_config($field) { return []; } /** * Build the control config array (runtime equivalent of build()). * * @param string $control_key Unique control key. * @param array $field Field configuration from stored schema. * @return array ['key' => string, 'responsive' => bool, 'args' => array] */ public function get_config($control_key, $field) { $label = !empty($field['label']) ? $field['label'] : $this->default_label(); $args = [ // translators: dynamic user-defined control label. 'label' => esc_html($label), 'type' => $this->type_constant(), ]; $args = array_merge($args, $this->get_type_specific_config($field)); $args = array_merge($args, $this->get_common_config($field)); return [ 'key' => $control_key, 'responsive' => !empty($field['responsive']), 'args' => $args, ]; } /** * Common control args (default, placeholder, description, label_block, * show_label, dynamic, frontend_available, separator, condition(s), selectors). * Array equivalent of build_common_properties(). * * @param array $field * @return array */ protected function get_common_config($field) { $config = []; if (isset($field['default'])) { $config['default'] = $field['default']; } if (!empty($field['placeholder'])) { // translators: dynamic user-defined placeholder. $config['placeholder'] = esc_html($field['placeholder']); } if (!empty($field['description'])) { // translators: dynamic user-defined description. $config['description'] = esc_html($field['description']); } if (isset($field['label_block'])) { $config['label_block'] = (bool) $field['label_block']; } if (isset($field['show_label'])) { $config['show_label'] = (bool) $field['show_label']; } if (!empty($field['dynamic'])) { $config['dynamic'] = ['active' => true]; } if (!empty($field['frontend_available'])) { $config['frontend_available'] = true; } if (!empty($field['separator']) && $field['separator'] !== 'default') { $config['separator'] = $field['separator']; } if (!empty($field['conditions']) && is_array($field['conditions'])) { $config = array_merge($config, $this->get_conditions_config($field['conditions'], $field)); } if (!empty($field['selectors']) && is_array($field['selectors'])) { $config = array_merge($config, $this->get_selectors_config($field['selectors'])); } elseif (!empty($field['selector']) && is_string($field['selector'])) { $config = array_merge($config, $this->get_single_selector_config($field)); } return $config; } /** * Conditions config (array equivalent of build_conditions/build_advanced_conditions). * * @param array $conditions_array * @param array $field * @return array */ protected function get_conditions_config($conditions_array, $field = []) { if (empty($conditions_array) || !is_array($conditions_array)) { return []; } $has_multiple = count($conditions_array) > 1; $has_non_equal = false; foreach ($conditions_array as $condition) { if (!empty($condition['equality']) && $condition['equality'] !== 'equal') { $has_non_equal = true; break; } } if ($has_multiple || $has_non_equal) { $operator_map = [ 'equal' => '===', 'not_equal' => '!==', 'in' => 'in', 'not_in' => '!in', 'contains' => 'contains', 'not_contains' => '!contains', ]; $terms = []; foreach ($conditions_array as $condition) { if (!empty($condition['control_name']) && isset($condition['control_value'])) { $equality = $condition['equality'] ?? 'equal'; $terms[] = [ 'name' => $this->convert_to_control_key($condition['control_name'], $field), 'operator' => $operator_map[$equality] ?? '===', 'value' => $condition['control_value'], ]; } } return $terms ? ['conditions' => ['terms' => $terms]] : []; } $condition = $conditions_array[0]; if (!empty($condition['control_name']) && isset($condition['control_value'])) { return [ 'condition' => [ $this->convert_to_control_key($condition['control_name'], $field) => $condition['control_value'], ], ]; } return []; } /** * Selectors config from the React selectors array (array equivalent of build_selectors). * * @param array $selectors_array * @return array */ protected function get_selectors_config($selectors_array) { if (empty($selectors_array) || !is_array($selectors_array)) { return []; } $out = []; foreach ($selectors_array as $item) { if (!empty($item['selector']) && !empty($item['properties'])) { $out[$this->normalize_selector($item['selector'])] = $item['properties']; } } return $out ? ['selectors' => $out] : []; } /** * Single-selector config (array equivalent of the inline selector handling * in build_common_properties). * * @param array $field * @return array */ protected function get_single_selector_config($field) { $default_property = 'color: {{VALUE}};'; if (isset($field['type'])) { $type = strtolower($field['type']); if (in_array($type, ['slider', 'number'], true)) { $default_property = '{{VALUE}}'; } elseif ($type === 'dimensions') { return []; } } return ['selectors' => [$this->normalize_selector($field['selector']) => $default_property]]; } /** * Build a group-control descriptor consumed by Dynamic_Widget::jltma_apply_control(). * Group controls register via add_group_control() (no 'type', no responsive method). * * @param string $control_key Control key (kept for descriptor shape; group uses args['name']). * @param string $group_class Fully-qualified Elementor Group_Control_* class. * @param array $args add_group_control args. * @return array */ protected function group_descriptor($control_key, $group_class, $args) { return [ 'key' => $control_key, 'method' => 'add_group_control', 'group_type' => call_user_func([$group_class, 'get_type']), 'args' => $args, ]; } /** * Partial common args used by Border/Box_Shadow group controls (show_label, * separator, conditions only — they do NOT emit the full common set). * * @param array $field * @return array */ protected function get_group_partial_common($field) { $common = []; if (isset($field['show_label'])) { $common['show_label'] = (bool) $field['show_label']; } if (!empty($field['separator']) && $field['separator'] !== 'default') { $common['separator'] = $field['separator']; } if (!empty($field['conditions']) && is_array($field['conditions'])) { $common = array_merge($common, $this->get_conditions_config($field['conditions'], $field)); } return $common; } /** * Ensure each selector part is scoped with {{WRAPPER}}. * * @param string $selector * @return string */ protected function normalize_selector($selector) { if (strpos($selector, ',') !== false) { $parts = array_map('trim', explode(',', $selector)); foreach ($parts as &$part) { if ($part !== '' && strpos($part, '{{WRAPPER}}') === false) { $part = '{{WRAPPER}} ' . $part; } } unset($part); return implode(', ', $parts); } return strpos($selector, '{{WRAPPER}}') === false ? '{{WRAPPER}} ' . $selector : $selector; } }