render($name, $value, $config, $showHidden, true); } public function render(string $name, mixed $value, array $config, bool $showHidden = false, bool $return = false): mixed { $out = ''; if (jvbCheck('hidden', $config) && !$showHidden) { return $out; } if (!array_key_exists('type', $config)) { return $out; } // Handle hidden display type if (array_key_exists('display', $config) && $config['display'] === 'hidden') { $out = ''; if (!$return) { echo $out; } return $out; } ob_start(); // Try custom function overrides first $type = array_map('ucfirst', explode('_', $config['type'])); $type = implode('', $type); $method = 'render' . $type . 'Field'; $nameTemp = implode('', array_map('ucfirst', explode('_', $name))); $nameMethod = 'render' . $nameTemp . 'Field'; if (function_exists($nameMethod)) { call_user_func($nameMethod, $value, $config); } elseif (function_exists($method)) { call_user_func($method, $value, $config); } elseif (method_exists($this, $method)) { $this->$method($name, $value, $config); } $out = ob_get_clean(); do_action('jvbRenderFormField', $name, $config, $value); $out = apply_filters('jvbFilterRenderFormField', $out, $name, $config, $value); if (!$return) { echo $out; } return $out; } /* ========== HELPER METHODS ========== */ /** * Prepare common field data */ protected function prepareFieldData(string $name, mixed $value, array $field): array { return [ 'name' => array_key_exists('group', $field) ? $field['group'] . '::' . $name : $name, 'value' => isset($field['value']) ? $field['value'] : $value, 'id' => (array_key_exists('base', $field) ? esc_attr($field['base']) : '') . esc_attr($name), ]; } /** * Build common HTML attributes for inputs */ protected function buildInputAttributes(string $name, array $field): string { $attrs = []; // Conditional rendering if (array_key_exists('condition', $field)) { $attrs['conditional'] = $this->handleConditionalField($field); } // Accessibility if (!empty($field['description'])) { $attrs['aria-describedby'] = $name . '-help'; } // Common attributes $common = ['placeholder', 'autocomplete', 'pattern', 'minlength', 'maxlength', 'min', 'max', 'step']; foreach ($common as $attr) { if (array_key_exists($attr, $field)) { $attrs[$attr] = $field[$attr]; } } // Required if (!empty($field['required'])) { $attrs['required'] = true; } // Build attribute string $attrString = ''; foreach ($attrs as $key => $val) { if ($key === 'conditional') { $attrString .= ' ' . $val; // Already formatted } elseif ($val === true) { $attrString .= ' ' . $key; } else { $attrString .= ' ' . $key . '="' . esc_attr($val) . '"'; } } return $attrString; } /** * Build validation data attributes */ protected function buildValidationAttributes(array $field): string { $attrs = []; if (!empty($field['pattern'])) { $attrs['data-pattern'] = $field['pattern']; } if (!empty($field['validate'])) { $attrs['data-validate'] = $field['validate']; } if (isset($field['min'])) { $attrs['data-min'] = $field['min']; } if (isset($field['max'])) { $attrs['data-max'] = $field['max']; } if (isset($field['minlength'])) { $attrs['data-minlength'] = $field['minlength']; } if (isset($field['maxlength'])) { $attrs['data-maxlength'] = $field['maxlength']; } if (!empty($field['validation_message'])) { $attrs['data-validation-message'] = $field['validation_message']; } $attrs['data-type'] = $field['type']; $attrString = ''; foreach ($attrs as $key => $val) { $attrString .= ' ' . $key . '="' . esc_attr($val) . '"'; } return $attrString; } /* ========== GENERIC FIELD WRAPPER ========== */ /** * Render a standard input field with validation wrapper */ protected function renderStandardInput(string $name, mixed $value, array $field, string $inputType = 'text'): void { $data = $this->prepareFieldData($name, $value, $field); $inputAttrs = $this->buildInputAttributes($name, $field); $validationAttrs = $this->buildValidationAttributes($field); $conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : ''; ?>
data-field="" > renderLabel($name, $field); ?>
>
renderHintAndDescription($field, $name); ?>
prepareFieldData($name, $field['value'] ?? '', $field); $validationAttrs = $this->buildValidationAttributes($field); $conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : ''; $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; // Additional data attributes for complex fields $dataAttrs = ''; if (array_key_exists('data', $field) && !empty($field['data'])) { foreach ($field['data'] as $key => $val) { $dataAttrs .= ($val === '') ? ' data-' . $key : ' data-' . $key . '="' . esc_attr($val) . '"'; } } ?>
data-field="" >

renderHintAndDescription($field, $name); ?>
renderHint($field['hint']); } if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } } /* ========== SIMPLE INPUT FIELD TYPES ========== */ public function renderTextField(string $name, mixed $value, array $field): void { $this->renderStandardInput($name, $value, $field, $field['input_type'] ?? 'text'); } public function renderEmailField(string $name, mixed $value, array $field): void { $field['validate'] = 'email'; // Auto-add email validation $this->renderStandardInput($name, $value, $field, 'email'); } private function renderUrlField(string $name, mixed $value, array $field): void { $field['validate'] = 'url'; // Auto-add URL validation $this->renderStandardInput($name, $value, $field, 'url'); } private function renderTelField(string $name, mixed $value, array $field): void { $field['validate'] = 'phone'; // Auto-add phone validation $this->renderStandardInput($name, $value, $field, 'tel'); } private function renderDateField(string $name, mixed $value, array $field): void { $format = !empty($field['format']) ? $field['format'] : 'Y-m-d'; // Format the date if we have a value if (!empty($value)) { $date = DateTime::createFromFormat($format, $value); if ($date) { $value = $date->format('Y-m-d'); // HTML date input requires Y-m-d format } } $this->renderStandardInput($name, $value, $field, 'date'); } private function renderTimeField(string $name, mixed $value, array $field): void { $this->renderStandardInput($name, $value, $field, 'time'); } private function renderDatetimeField(string $name, mixed $value, array $field): void { $this->renderStandardInput($name, $value, $field, 'datetime-local'); } /* ========== TEXTAREA FIELD ========== */ public function renderTextareaField(string $name, mixed $value, array $field): void { $data = $this->prepareFieldData($name, $value, $field); $inputAttrs = $this->buildInputAttributes($name, $field); $validationAttrs = $this->buildValidationAttributes($field); $conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : ''; $rows = isset($field['rows']) ? (int)$field['rows'] : 4; $quill = (array_key_exists('quill', $field) && $field['quill'] == true) ? ' data-editor="true"' : ''; if ($quill !== '') { $allowImages = array_key_exists('allowImage', $field); $quill .= ($allowImages) ? ' data-allowimage="true"' : ' data-allowimage="false"'; } // Handle array values if (is_array($value)) { $value = implode(', ', $value); } ?>
data-field="" > renderLabel($name, $field); ?>
renderHintAndDescription($field, $name); ?>
prepareFieldData($name, $value, $field); $conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : ''; $validationAttrs = $this->buildValidationAttributes($field); $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; $min = isset($field['min']) ? (float)$field['min'] : 0; $max = isset($field['max']) ? (float)$field['max'] : 100; $step = isset($field['step']) ? (float)$field['step'] : 1; // Handle custom data attributes $customData = ''; if (array_key_exists('data', $field) && !empty($field['data'])) { foreach ($field['data'] as $key => $v) { $customData .= ($v === '') ? ' data-' . $key : ' data-' . $key . '="' . $v . '"'; } } if (empty($value)) { $value = $field['default'] ?? 0; } $autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="' . $field['autocomplete'] . '"' : ''; ?>
data-field="" > renderLabel($name, $field); ?>
> >
renderHintAndDescription($field, $name); ?>
prepareFieldData($name, $value, $field); $inputAttrs = $this->buildInputAttributes($name, $field); $validationAttrs = $this->buildValidationAttributes($field); $conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : ''; ?>
data-field="" > renderLabel($name, $field); ?>
renderHintAndDescription($field, $name); ?>
prepareFieldData($name, $value, $field); $validationAttrs = $this->buildValidationAttributes($field); $conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : ''; ?>
data-field="" >
* $label) : ?>
renderHintAndDescription($field, $name); ?>
prepareFieldData($name, $value, $field); $validationAttrs = $this->buildValidationAttributes($field); $conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : ''; if (!is_array($value)) { $value = !empty($value) ? [$value] : []; } ?>
data-field="" >
* $label) : ?> >
renderHintAndDescription($field, $name); ?>
prepareFieldData($name, $value, $field); $validationAttrs = $this->buildValidationAttributes($field); $conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : ''; $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; ?>
data-field="" > renderHintAndDescription($field, $name); ?>
renderComplexFieldWrapper($name, $field, function($name, $data, $field) use ($value) { $values = is_array($value) ? $value : []; $rowLabel = $field['row_label'] ?? ''; $rowTitle = $field['new_row'] ?? 'New Item'; $addLabel = $field['add_label'] ?? 'Add Item'; ?>
$row) { $this->renderRepeaterRow($field['fields'], $row, $index, $name, $rowTitle); } } ?>
> # getRowTitle($fields, $values, $rowTitle)) ?>
$field) { $field_name = ($base_name === '') ? $slug : sprintf('%s:%s:%s', $base_name, $index, $slug); $field_value = $values[$slug] ?? ''; $this->render($field_name, $field_value, $field); } ?>
$field) { if (in_array($field['type'], ['text', 'textarea']) && isset($values[$slug]) && !empty($values[$slug])) { return $values[$slug]; } } return $rowTitle; } /* ========== GROUP FIELD ========== */ protected function renderGroupField(string $name, mixed $value, array $field): void { if (!array_key_exists('fields', $field) || empty($field['fields'])) { return; } $values = is_array($value) ? $value : []; $original = $name; if (array_key_exists('group', $field)) { $name = $field['group'] . '::' . $name; } $hidden = (array_key_exists('mode', $field) && $field['mode'] === 'hidden'); if ($hidden) { // Simplified render for hidden groups $this->renderGroupFields($name, $values, $field); return; } // Standard fieldset render $conditional = $this->handleConditionalField($field); $validationAttrs = $this->buildValidationAttributes($field); $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; ?>
data-field="" > renderHintAndDescription($field, $name); ?>
renderGroupFields($name, $values, $field); ?>
$config) { // Set the group context for proper field naming $config['group'] = $groupName; // Get the value for this specific field $field_value = $values[$field_name] ?? ''; // Handle conditional fields within the group if (isset($config['condition'])) { $condition_field = $config['condition']['field']; if (!str_contains($condition_field, '::')) { $config['condition']['field'] = $groupName . '::' . $condition_field; } } $this->render($field_name, $field_value, $config); } } /* ========== UPLOAD FIELD ========== */ private function renderUploadField(string $name, mixed $value, array $field): void { // Merge with defaults $config = array_merge([ 'subtype' => 'image', 'accepted_types' => null, 'multiple' => false, 'limit' => 0, 'mode' => 'direct', 'destination' => 'meta', 'max_size' => null, 'convert' => 'webp', 'quality' => 80, 'create_thumbnails' => true, ], $field); // Validate destination config if (in_array($config['destination'], ['post', 'post_group']) && empty($config['content'])) { error_log("Upload field '{$name}' has destination '{$config['destination']}' but no content defined"); return; } if (array_key_exists('group', $field)) { $name = $field['group'] . '::' . $name; } // Prepare upload configuration $acceptedTypes = $this->getAllowedTypes($config); $acceptExtensions = $this->getMimeExtensions($acceptedTypes); $acceptAttr = implode(',', $acceptExtensions); $attachmentIds = $this->parseAttachmentIds($value); $fieldType = $config['multiple'] ? 'gallery' : 'image'; // Build data attributes for uploader.js $uploadData = [ 'data-subtype' => $config['subtype'], 'data-mode' => $config['mode'], 'data-destination' => $config['destination'], 'data-multiple' => $config['multiple'] ? 'true' : 'false', 'data-limit' => $config['limit'], 'data-convert' => $config['convert'], 'data-quality' => $config['quality'], ]; if (!empty($config['content'])) { $uploadData['data-content'] = $config['content']; } $this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use ( $config, $acceptAttr, $attachmentIds, $fieldType, $uploadData, $value ) { ?>
$val) : ?> ="" >
renderUploadPreviews($attachmentIds, $config); ?>
>

Click to upload or drag and drop
getUploadInstructions($config)) ?>

>
'; echo ''; echo ''; echo ''; } } // Add other subtypes (video, document) as needed } } /** * Get upload instruction text based on config */ private function getUploadInstructions(array $config): string { $extensions = $this->getMimeExtensions($this->getAllowedTypes($config)); $extList = implode(', ', array_map('strtoupper', $extensions)); $maxSize = $config['max_size'] ?? $this->max_file_size; $maxSizeMB = round($maxSize / 1048576, 1); return "{$extList} (max. {$maxSizeMB}MB)"; } /* ========== TAXONOMY/USER SELECTOR FIELDS ========== */ private function renderTaxonomyField(string $name, mixed $value, array $field): void { if (array_key_exists('group', $field)) { $name = $field['group'] . '::' . $name; } $this->renderSelectorField($name, $value, $field, 'taxonomy'); } private function renderUserField(string $name, mixed $value, array $field): void { if (array_key_exists('group', $field)) { $name = $field['group'] . '::' . $name; } $this->renderSelectorField($name, $value, $field, 'post'); } /** * Generic selector field renderer * Handles both taxonomy and post selectors with consistent structure */ private function renderSelectorField(string $name, mixed $value, array $field, string $type): void { $conditional = $this->handleConditionalField($field); $validationAttrs = $this->buildValidationAttributes($field); $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; // Parse selected values $selected = ($value === '') ? [] : (is_array($value) ? $value : explode(',', $value)); // Create selector instance if ($type === 'taxonomy') { $taxonomy = $field['taxonomy']; $selectorConfig = [ 'multiple' => $field['multiple'] ?? true, 'placeholder' => $field['placeholder'] ?? 'Search terms...', 'noResults' => 'No terms found', 'onClose' => 'updateMetaFormTaxonomy' ]; $selector = new TaxonomySelector($taxonomy, $selectorConfig); $icon = $taxonomy; } else { $postType = $field['post_type']; $selectorConfig = [ 'multiple' => $field['multiple'] ?? true, 'placeholder' => $field['placeholder'] ?? 'Search posts...', 'noResults' => 'No posts found', 'shop_id' => $field['shop_id'] ?? null, 'onClose' => 'updateMetaFormPost' ]; $selector = new PostSelector($postType, $selectorConfig); $icon = $postType; } $containerId = $name . '-' . $type . '-selector'; ?>
data-field="" >
render($selected, $containerId) ?> ="" value="" > renderHintAndDescription($field, $name); ?>
connect('maps'); if (!$googleMaps->isSetUp()) { echo '

Google Maps not configured. Please configure in Integrations settings.

'; return; } // Parse stored data if (is_string($value)) { $value = maybe_unserialize($value); } $stored_data = is_array($value) ? $value : []; $address = $stored_data['address'] ?? ''; $lat = $stored_data['lat'] ?? ''; $lng = $stored_data['lng'] ?? ''; $street = $stored_data['street'] ?? ''; if (array_key_exists('group', $field)) { $name = $field['group'] . '::' . $name; } // Prepare JavaScript configuration $field_id = esc_attr($name); $map_id = $field_id . '_map'; $js_config = [ 'fieldId' => $field_id, 'initialCoords' => (!empty($lat) && !empty($lng)) ? [ 'lat' => (float)$lat, 'lng' => (float)$lng ] : null ]; $this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use ( $stored_data, $street, $address, $lat, $lng, $map_id, $js_config ) { ?>

Current location:

>
$method_name(); } if ($content === '') { return; } echo sprintf( '
%s
', esc_attr($name), $content ); } /* ========== UTILITY METHODS ========== */ private function handleConditionalField(array $field):string { if (empty($field['condition'])) { return ''; } $condition = $field['condition']; return sprintf( 'data-depends-on="%s" data-depends-value="%s" data-depends-operator="%s"', esc_attr($field['condition']['field']), esc_attr($field['condition']['value']), esc_attr($field['condition']['operator'] ?? '==') ); } protected function renderHint(string $hint): void { ?>

['image/jpeg', 'image/png', 'image/gif', 'image/webp'], 'video' => ['video/mp4', 'video/webm', 'video/ogg'], 'document' => ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], 'any' => ['image/*', 'video/*', 'application/pdf'] ]; return $defaults[$config['subtype']] ?? $defaults['image']; } protected function getMimeExtensions(array $mimeTypes): array { $extensions = []; foreach ($mimeTypes as $mime) { if (str_contains($mime, '*')) { continue; // Skip wildcards } $ext = str_replace(['image/', 'video/', 'application/'], '', $mime); $extensions[] = '.' . $ext; } return $extensions; } protected function parseAttachmentIds(mixed $value): array { if (empty($value)) { return []; } if (is_array($value)) { return array_filter($value, 'is_numeric'); } if (is_string($value)) { return array_filter(explode(',', $value), 'is_numeric'); } return is_numeric($value) ? [$value] : []; } }