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="= esc_attr($name) ?>"
= $validationAttrs ?>>
renderLabel($name, $field); ?>
= $customData?>
>
= jvbIcon('check-circle') ?>
= jvbIcon('x-circle') ?>
renderHintAndDescription($field, $name); ?>
= esc_html($field['label']) ?>
*
0 /= esc_attr($field['limit']) ?>
renderHint($field['hint']);
}
if (!empty($field['description'])) {
$this->renderDescription($field['description'], $name);
}
}
protected function renderHint(string $hint): void
{
?>
= esc_html($hint) ?>
= wp_kses_post($description) ?>
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="= esc_attr($name) ?>"
= $validationAttrs ?>>
renderLabel($name, $field); ?>
= jvbIcon('check-circle') ?>
= jvbIcon('x-circle') ?>
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="= esc_attr($name) ?>"
= $validationAttrs ?>>
renderLabel($name, $field); ?>
>
= jvbIcon('minus-square') ?>
= $autocomplete ?>
= !empty($field['required']) ? 'required' : '' ?>>
= jvbIcon('plus-square') ?>
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="= esc_attr($name) ?>"
= $validationAttrs ?>>
renderLabel($name, $field); ?>
>
$label) : ?>
>
= esc_html($label) ?>
= jvbIcon('check-circle') ?>
= jvbIcon('x-circle') ?>
renderHintAndDescription($field, $name); ?>
prepareFieldData($name, $value, $field);
$validationAttrs = $this->buildValidationAttributes($field);
$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
?>
data-field="= esc_attr($name) ?>"
= $validationAttrs ?>>
= esc_html($field['label']) ?>
*
$label) : ?>
= !empty($field['required']) ? 'required' : '' ?>
>
= $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="= esc_attr($name) ?>"
= $validationAttrs ?>>
= esc_html($field['label']) ?>
*
$label) : ?>
>
= esc_html($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="= esc_attr($name) ?>"
= $validationAttrs ?>>
>
= !empty($field['required']) ? 'required' : '' ?>
>
*
= esc_html($field['label']) ?>
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_label ? 'data-label="' . esc_attr($row_label) . '"' : ''; ?>
=$conditional?>>
= esc_html($field['label']); ?>
$row) {
$this->renderRepeaterRow($field['fields'], $row, $index, $name, $rowTitle);
}
}
?>
renderRepeaterRow($field['fields'], array(), '', '', $rowTitle); ?>
= jvbIcon('plus', ['title'=> 'Add']); ?> = (array_key_exists('add_label', $field)) ? $field['add_label'] : 'Add Item'; ?>
renderHint($field['hint']); } ?>
renderDescription($field['description'], $name); } ?>
>
$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';
?>
<= $fieldset?> class="field group = esc_attr($name) ?>"
= $conditional ?>
data-field="= esc_attr($name) ?>"
= $validationAttrs ?>
= $describedBy ?>>
<=$legend?>>= esc_html($field['label']) ?>=$legend?>>
renderHintAndDescription($field, $name); ?>
renderGroupFields($name, $values, $field); ?>
= $fieldset?>>
$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) . '"' : '');
}
?>
= $conditional ?>>
= !empty($field['required']) ? 'required' : '' ?>>
= esc_html($field['label']) ?>
= esc_html($field['description']) ?>
Click to upload or drag and drop
= esc_html($this->getAcceptedTypesLabel($subtype, $acceptExtensions)) ?>
(max. = esc_html($this->formatFileSize($this->getMaxFileSize($subtype))) ?>)
You can group images to create separate = $plural ?>.
If a =$singular?> has multiple images, you can select the = jvbIcon('star')?> to set an image as the main one.
= esc_html($field['upload_description']); ?>
= jvbIcon('plus-square') ?>
Group
= jvbIcon('trash') ?>
Delete
= jvbIcon('cloud-arrow-up') ?> Upload = esc_html($plural ?? 'Content'); ?>
Processing files...
0/0 '); ?>
renderExistingAttachment($attachmentId, $subtype);
}
?>
= jvbIcon('arrow-elbow-left-up') ?> These will become individual = $plural ?> = jvbIcon('arrow-elbow-right-up')?>
>
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] : '';
?>
>
=jvbIcon('pencil-simple')?>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] : '';
?>
>
';
=jvbIcon('pencil-simple')?>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] : '';
?>
>
';
=jvbIcon('pencil-simple')?>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="= esc_attr($name) ?>"
= $validationAttrs ?>
= $describedBy ?>>
= $selector->render($selected) ?>
="= esc_attr($field[$type === 'taxonomy' ? 'taxonomy' : 'post_type']) ?>"
value="= esc_attr(is_array($selected) ? implode(',', $selected) : $value) ?>"
= !empty($field['required']) ? 'required' : '' ?>>
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';
?>
= $conditional ?>
= $validationAttrs ?>>
= esc_html($field['label']) ?>
$subfield_config): ?>
render($input_name, '', $subfield_config, false, false);
?>
= jvbIcon('plus') ?> = $field['add_label'] ?? 'Add' ?>
$item_data): ?>
renderTagItem($field['fields'], $item_data, $index, $name, $tagFormat); ?>
renderTagItem($field['fields'], [], '', $name, $tagFormat); ?>
renderHint($field['hint']); ?>
renderDescription($field['description'], $name); ?>
getTagDisplayText($fields, $data, $format);
?>
= esc_html($tag_text) ?>
$field_config): ?>
= jvbIcon('x') ?>
$value) {
$text = str_replace('{' . $key . '}', $value, $text);
}
return $text;
}
// Use specific field name
return $data[$format] ?? 'New Item';
}
}
}