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; } if (!$value) { $value = $this->getDefaultValue($config['type']); } // 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; } public function getDefaultValue(string $type):mixed { if (!$this->type_manager) { $this->type_manager = new MetaTypeManager(); } return match ($this->type_manager->getMetaType($type)) { 'object', 'array' => [], 'boolean' => false, 'integer' => 0, default => '', }; } /* ========== 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) : ''; $customData = ''; if (array_key_exists('data', $field) && !empty($field['data'])) { foreach ($field['data'] as $key => $v) { $customData .= ($v === '') ? ' data-' . $key : ' data-' . $key . '="' . $v . '"'; } } ?>
data-field="" > renderLabel($name, $field); ?>
>
renderHintAndDescription($field, $name); ?>
renderHint($field['hint']); } if (!empty($field['description'])) { $this->renderDescription($field['description'], $name); } } protected function renderHint(string $hint): void { ?>

renderStandardInput($name, $value, $field, $field['subtype'] ?? '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); ?>
handleConditionalField($field); $row_label = isset($field['row_label']) ? $field['row_label'] : ''; $rowTitle = (array_key_exists('new_row', $field)) ? $field['new_row'] : 'New Item'; if (array_key_exists('group', $field)) { $name = $field['group'].'::'.$name; } $describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : ''; ?>
>

$row) { $this->renderRepeaterRow($field['fields'], $row, $index, $name, $rowTitle); } } ?>
renderHint($field['hint']); } ?> renderDescription($field['description'], $name); } ?>
> # getRowTitle($fields, $values, $rowTitle)); ?>
$field) : if ($base_name === '') { $field_name = $slug; } else { $field_name = sprintf('%s:%s:%s', $base_name, $index, $slug); } $field_value = isset($values[$slug]) ? $values[$slug] : ''; $name = $field_name; $this->render($name, $field_value, $field); endforeach; ?>
$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'])) { error_log('No fields to render'); 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"' : ''; $fieldset = (array_key_exists('wrap', $field) && $field['wrap'] === 'details') ? 'details' : 'fieldset'; $legend = (array_key_exists('wrap', $field) && $field['wrap'] === 'details') ? 'summary' : 'legend'; ?> < class="field group " 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 renderGalleryField(string $name, mixed $value, array $field):void { $field['multiple'] = true; $this->renderUploadField($name, $value, $field); } private function renderUploadField(string $name, mixed $value, array $field): void { $defaultConfig = [ //File Type 'subtype' => 'image', // 'image', 'video', 'document', 'any' 'accepted_types' => null, // null = use subtype defaults, or array of specific MIME types //Upload Behaviour 'multiple' => false, // Single or multiple uploads 'limit' => 0, // Max number of uploads (0 = unlimited) 'mode' => 'direct', // 'direct' or 'selection' //Destination 'destination' => 'meta', // 'meta', 'post', 'post_group' //Processing Options 'max_size' => null, // Override default size limits 'convert' => 'webp', // Image conversion format 'quality' => 80, // Conversion quality 'create_thumbnails' => true, ]; $config = array_merge($defaultConfig, $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; } // Get accepted types $acceptedTypes = $this->getAllowedTypes($config); // Build accept attribute for input $acceptExtensions = $this->getMimeExtensions($acceptedTypes); $acceptAttr = implode(',', $acceptExtensions); // Determine field attributes $subtype = $config['subtype'] ?? 'image'; $multiple = $config['multiple'] ?? false; $limit = $config['limit'] ?? 0; $mode = $config['mode'] ?? 'direct'; $destination = $config['destination']; // Get existing attachments $attachmentIds = $this->parseAttachmentIds($value); // Determine field type for UI $fieldType = $multiple ? 'gallery' : 'single'; // Build data attributes $dataAttrs = [ 'data-field' => $name, 'data-upload-field' => '', 'data-mode' => $mode, 'data-type' => $fieldType, 'data-subtype' => $subtype, 'data-destination' => $destination, ]; if (!empty($field['content'])) { $dataAttrs['data-content'] = $field['content']; } if ($limit > 0) { $dataAttrs['data-limit'] = $limit; } // Build data attributes $conditional = $this->handleConditionalField($field); $describedBy = !empty($field['description']) ? ' aria-describedby="' . esc_attr($name) . '-help"' : ''; if (!empty($field['group'])) { $name = $field['group'] . '::' . $name; } // Convert data attributes to string $dataAttrString = ''; foreach ($dataAttrs as $attr => $val) { $dataAttrString .= ' ' . $attr . ($val !== '' ? '="' . esc_attr($val) . '"' : ''); } ?>
>
>

Click to upload or drag and drop
getAcceptedTypesLabel($subtype, $acceptExtensions)) ?> (max. formatFileSize($this->getMaxFileSize($subtype))) ?>)

You can group images to create separate .

If a has multiple images, you can select the to set an image as the main one.

>
renderImagePreview($attachmentId); break; case 'video': $this->renderVideoPreview($attachmentId); break; case 'document': case 'file': $this->renderFilePreview($attachmentId); break; default: $this->renderImagePreview($attachmentId); break; } return ob_get_clean(); } /** * Get max file size for subtype */ private function getMaxFileSize(string $subtype): int { $sizes = [ 'image' => 5242880, // 5MB 'video' => 104857600, // 100MB 'document' => 10485760 // 10MB ]; return $sizes[$subtype] ?? $sizes['image']; } /** * Format file size for display */ private function formatFileSize(int $bytes): string { if ($bytes >= 1073741824) { return number_format($bytes / 1073741824, 1) . 'GB'; } if ($bytes >= 1048576) { return number_format($bytes / 1048576, 1) . 'MB'; } if ($bytes >= 1024) { return number_format($bytes / 1024, 1) . 'KB'; } return $bytes . 'B'; } /** * Get accepted types label */ private function getAcceptedTypesLabel(string $subtype, array $extensions): string { $labels = [ 'image' => 'JPG, PNG, GIF, or WEBP', 'video' => 'MP4, WEBM, or MOV', 'document' => 'PDF, DOC, XLS, or TXT', 'any' => 'Images, Videos, or Documents' ]; return $labels[$subtype] ?? strtoupper(implode(', ', array_map(function($ext) { return ltrim($ext, '.'); }, array_slice($extensions, 0, 3)))); } /** * Render upload preview items */ private function renderUploadPreviews(array $attachmentIds, array $config): void { if (empty($attachmentIds)) { return; } foreach ($attachmentIds as $id) { switch ($config['subtype']) { case 'image': $this->renderImagePreview($id, $config); break; case 'video': $this->renderVideoPreview($id, $config); break; case 'file': $this->renderFilePreview($id, $config); break; } } } public function renderImagePreview(?int $id = null, array $config = []):void { $attachment = ($id) ? wp_get_attachment_image($id, 'thumbnail', false) : false; $caption = ($id) ? wp_get_attachment_caption($id) : ''; $alt = ($id) ? get_post_meta($id, '_wp_attachment_image_alt',true) : ''; $title = ($id) ? get_the_title($id) : ''; $addID = ($id) ? '-'.$id : ''; $dataID = ($id) ? ['id' => $id] : ''; ?>
>
Edit Info [ 'type' => 'group', 'wrap' => 'details', 'label' => 'Image Info', 'hint' => 'These will be automatically generated if left blank.', 'fields' => [ 'image-title'.$addID => [ 'type' => 'text', 'label' => 'Image Title', 'value' => $title, 'data' => $dataID ], 'image-alt-text'.$addID => [ 'type' => 'text', 'label' => 'Alt Text', 'value' => $alt, 'hint' => 'Alt text helps the visually impaired, as well as some benefits for SEO.', 'data' => $dataID ], 'image-caption'.$addID => [ 'type' => 'textarea', 'value' => $caption, 'label' => 'Image Caption', 'data' => $dataID ] ] ] ], $fields); $meta = new MetaManager($id); foreach ($fields as $field => $config) { $meta->render('form', $field, $config); } ?>
$id] : ''; ?>
>
'; Edit Info [ 'type' => 'group', 'wrap' => 'details', 'label' => 'Video Info', 'hint' => 'These will be automatically generated if left blank.', 'fields' => [ 'title' => [ 'type' => 'text', 'label' => 'Video Title', 'value' => $title, 'data' => $dataID ], 'caption' => [ 'type' => 'textarea', 'value' => $caption, 'label' => 'Video Caption', 'data' => $dataID ], 'description' => [ 'type' => 'textarea', 'value' => $description, 'label' => 'Video Description', 'data' => $dataID ] ] ] ], $fields); $this->render('upload_data', null, $fields); ?>
$id] : ''; ?>
>
'; Edit Info [ 'type' => 'group', 'wrap' => 'details', 'label' => 'File Info', 'hint' => 'These will be automatically generated if left blank.', 'fields' => [ 'title' => [ 'type' => 'text', 'label' => 'File Title', 'value' => $title, 'data' => $dataID ], 'caption' => [ 'type' => 'textarea', 'value' => $caption, 'label' => 'File Caption', 'data' => $dataID ], 'description' => [ 'type' => 'textarea', 'value' => $description, 'label' => 'File Description', 'data' => $dataID ] ] ] ], $fields); $this->render('upload_data', null, $fields); ?>
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, string $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, string $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"' : ''; $isSimple = (array_key_exists('mode', $field) && $field['mode']==='simple'); // Parse selected values $value = (is_array($value)) ? array_filter(array_map('absint', $value)): $value; $selected = ($value === '') ? [] : (is_array($value) ? $value : explode(',', $value)); // Generate unique container ID $containerId = $name . '-' . $type . '-selector'; // Create selector instance with proper parameters if ($type === 'taxonomy') { $taxonomy = $field['taxonomy']; $icon = JVB_TAXONOMY[$taxonomy]['icon']??''; // Map field config to selector config $selectorConfig = [ 'max' => $field['max'] ?? 0, // 0 = unlimited 'search' => $field['search'] ?? true, 'label' => $field['label'] ?? '', 'createNew' => $field['createNew'] ?? false, 'required' => $field['required'] ?? false, 'base' => $field['base'] ?? '', 'update' => $field['update'] ?? true, 'name' => $name, 'autocomplete' => $field['autocomplete'] ?? false, ]; if ($icon !== '') { $selectorConfig['icon'] = $icon; } $selector = new TaxonomySelector($containerId, $taxonomy, $selectorConfig); $icon = $taxonomy; } else { $postType = $field['post_type']; // Map field config to selector config $selectorConfig = [ 'max' => $field['max'] ?? 0, 'search' => $field['search'] ?? true, 'label' => $field['label'] ?? '', 'required' => $field['required'] ?? false, 'base' => $field['base'] ?? '', 'update' => $field['update'] ?? true, 'shop_id' => $field['shop_id'] ?? null, 'autocomplete'=> $field['autocomplete'] ?? true, ]; $selector = new PostSelector($containerId, $postType, $selectorConfig); $icon = $postType; } ?>
data-field="" > render($selected) ?> ="" value="" > renderHintAndDescription($field, $name); ?>
connect('maps'); if (!$googleMaps->isSetUp()) { echo '

Google Maps not configured. Please configure in Integrations settings.

'; return; } // Extract stored values 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'] ?? ''; // Generate unique field ID $field_id = esc_attr($name); $map_id = $field_id . '_map'; // Handle grouped fields if (array_key_exists('group', $field)) { $name = $field['group'] . '::' . $name; } $describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : ''; // Prepare configuration for JavaScript initialization $js_config = [ 'fieldId' => $field_id, 'initialCoords' => (!empty($lat) && !empty($lng)) ? [ 'lat' => (float)$lat, 'lng' => (float)$lng ] : null ]; // IMPORTANT: Properly escape the JSON for use in HTML attribute $json_config = htmlspecialchars(json_encode($js_config), ENT_QUOTES, 'UTF-8'); ?>
> Current location: '.esc_html($stored_data['street']).'

'; echo '

Search below to change:

'; } ?> renderHint($field['hint']); } ?> renderDescription($field['description'], $name); } ?>
$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 getAllowedTypes(array $config): array { if (!empty($config['accepted_types'])) { return $config['accepted_types']; } // Default types based on subtype $defaults = [ 'image' => ['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] : []; } /** * Render tag list field - inline tag input interface */ protected function renderTagListField(string $name, mixed $value, array $field): void { $values = is_array($value) ? $value : []; $conditional = $this->handleConditionalField($field); $validationAttrs = $this->buildValidationAttributes($field); if (array_key_exists('group', $field)) { $name = $field['group'] . '::' . $name; } $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; // Tag display format - defaults to first field value $tagFormat = $field['tag_format'] ?? 'first_field'; ?>
>

$subfield_config): ?> render($input_name, '', $subfield_config, false, false); ?>
$item_data): ?> renderTagItem($field['fields'], $item_data, $index, $name, $tagFormat); ?>
renderHint($field['hint']); ?> renderDescription($field['description'], $name); ?>
getTagDisplayText($fields, $data, $format); ?>
$field_config): ?>
$value) { $text = str_replace('{' . $key . '}', $value, $text); } return $text; } // Use specific field name return $data[$format] ?? 'New Item'; } } }