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="= esc_attr($name) ?>"
= $validationAttrs ?>>
renderLabel($name, $field); ?>
>
= jvbIcon('check-circle') ?>
= jvbIcon('x-circle') ?>
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="= esc_attr($name) ?>"
= $validationAttrs ?>
= $dataAttrs ?>
= $describedBy ?>>
= esc_html($field['label']) ?>
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="= 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); ?>
>
= $autocomplete ?>
= !empty($field['required']) ? 'required' : '' ?>>
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); ?>
= 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 ?>>
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 ?>>
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 ?>>
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);
}
}
?>
renderRepeaterRow($field['fields'], [], '', $name, $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"' : '';
?>
$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) : ?>
= $attr ?>="= esc_attr($val) ?>"
>
renderUploadPreviews($attachmentIds, $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="= esc_attr($name) ?>"
= $validationAttrs ?>
= $describedBy ?>>
= $selector->render($selected, $containerId) ?>
="= 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;
}
// 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: = esc_html($street) ?>
>
$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
{
?>
= esc_html($hint) ?>
= wp_kses_post($description) ?>
['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] : [];
}
}