'number', 'label' => 'Price']); * echo Form::render('email', '', ['type' => 'email', 'required' => true]); */ class Form { /** * Render a form field based on type */ public static function render(string $name, mixed $value, array $config = []): string { if ($value === null) { $value = ''; } if (!empty($config['hidden'])) { return ''; } $type = $config['type'] ?? 'text'; $method = 'render' . str_replace('_', '', ucwords($type, '_')); $output = method_exists(static::class, $method) ? static::$method($name, $value, $config) : static::renderText($name, $value, $config); return apply_filters('jvbRenderFormMeta', $output, $name, $config, $value, null); } /** * Render with Meta instance (convenience method) */ public static function renderFrom(Meta $meta, string $name): string { $value = $meta->get($name); $config = $meta->config($name) ?? ['type' => 'text']; return static::render($name, $value, $config); } /** * Render complete form from Meta instance */ public static function renderFormFrom(Meta $meta, string $endpoint, array $options = []): string { $id = $options['form-id'] ?? $endpoint; $classes = isset($options['classes']) ? ' class="' . implode(' ', $options['classes']) . '"' : ''; $output = '
'; if (!empty($options['heading'])) { $output .= '

' . esc_html($options['heading']) . '

'; } if (!empty($options['description'])) { $descriptions = is_array($options['description']) ? $options['description'] : [$options['description']]; foreach ($descriptions as $d) { $output .= '

' . esc_html($d) . '

'; } } foreach ($meta->configs() as $name => $config) { $output .= static::render($name, $meta->get($name), $config); } if (!empty($options['submit'])) { $output .= ''; } $output .= '
'; return $output; } // ───────────────────────────────────────────────────────────── // Helper Methods // ───────────────────────────────────────────────────────────── public static function fieldWrap(string $name, string $content, array $config): string { $classes = static::buildClasses($config); $datasets = static::buildDatasets($config); if (!array_key_exists('type', $config)) { error_log('Config without type: '.print_r($config, true)); } $output = sprintf( '
', $classes, $name, str_replace('_', '-', $config['type']), $datasets ); $output .= static::buildCharacterLimit($config); $output .= static::buildLabel($name, $config); if (!array_key_exists('skipInput', $config)) { $output .= static::buildInput($content); } else { $output .= $content; } $output .= static::buildHint($config); $output .= static::buildDescription($name, $config); $output .= '
'; return $output; } protected static function buildClasses(array $config): string { $classes = ['field '. (str_replace('_','-',sanitize_text_field($config['type'] ?? 'text')))]; if (!empty($config['required'])) { $classes[] = 'required'; } if (!empty($config['class'])) { if (!is_array($config['class'])) { $config['class'] = [$config['class']]; } $classes = array_merge($classes, $config['class']); } return trim(implode(' ',$classes)); } protected static function buildDatasets(array $config): string { $datasets = static::handleCustomDatasets($config); $datasets .= static::handleValidationLogic($config); $datasets .= static::handleConditionalLogic($config); return $datasets; } protected static function handleCustomDatasets($config):string { if (array_key_exists('data', $config) && !empty($config['data'])) { $datasets = array_map(function ($value, $key) use ($config) { $name = str_replace('_', '-', sanitize_title($key)); return ' data-'.$name.'="'.$value.'"'; }, array_values($config['data']), array_keys($config['data'])); return implode($datasets); } return ''; } protected static function handleValidationLogic($config):string { $datasets = ''; $dataAttrs = ['pattern', 'validate', 'min', 'max', 'minlength', 'maxlength', 'validation_message']; $attrs = []; foreach ($dataAttrs as $attr) { if (array_key_exists($attr, $config) && !empty($config[$attr])) { $attrs[$attr] = $config[$attr]; } } foreach($attrs as $attr => $value) { $datasets .= sprintf(' data-%s="%s"', $attr, esc_attr($value)); } return $datasets; } protected static function handleConditionalLogic($config):string { if (empty($config['condition'])) { return ''; } return sprintf( 'data-depends-on="%s" data-depends-value="%s" data-depends-operator="%s"', esc_attr($config['condition']['field']), esc_attr($config['condition']['value']), esc_attr($config['condition']['operator'] ?? '==') ); } protected static function buildCharacterLimit(array $config): string { $type = $config['type'] ?? 'text'; if (in_array($type, ['upload', 'gallery', 'selector'])) { return ''; } if (empty($config['maxlength'])) { return ''; } return sprintf( '0 / %s', esc_html($config['maxlength']) ); } protected static function buildLabel(string $name, array $config):string { if (!empty($config['label'])) { return sprintf( '', esc_attr($name), esc_html($config['label']), !empty($config['required']) ? '*' : '' ); } return ''; } public static function buildInput(string $content):string { return sprintf( '
%s
', $content, jvbIcon('check-circle'), jvbIcon('x-circle') ); } protected static function buildHint(array $config):string { if (!empty($config['hint'])) { return sprintf( '%s', esc_html($config['hint']) ); } return ''; } protected static function buildDescription(string $name, array $config):string { if (!empty($config['description'])) { return sprintf( '

%s

', esc_attr($name), esc_html($config['description']) ); } return ''; } protected static function inputAttrs(string $name, array $config): string { $attrs = [ 'id' => $name, 'name' => $name, ]; if (!empty($config['placeholder'])) { $attrs['placeholder'] = $config['placeholder']; } if (!empty($config['autocomplete'])) { $attrs['autocomplete'] = $config['autocomplete']; } if (isset($config['min'])) { $attrs['min'] = $config['min']; } if (isset($config['max'])) { $attrs['max'] = $config['max']; } if (isset($config['step'])) { $attrs['step'] = $config['step']; } $html = ''; //Add the attributes that stand on their own $standalones = ['required', 'disabled', 'readonly','multiple']; foreach($standalones as $s) { if (array_key_exists($s, $config) && $config[$s] === true) { $html .= ' '.$s; } } foreach ($attrs as $key => $val) { $html .= sprintf(' %s="%s"', $key, esc_attr($val)); } return $html; } // ───────────────────────────────────────────────────────────── // Type Renderers // ───────────────────────────────────────────────────────────── protected static function renderText(string $name, mixed $value, array $config): string { $value = ($value !== '') ? ' value="'.esc_attr($value).'"' : ''; $input = sprintf( '', $config['subtype']??'text', $value, static::inputAttrs($name, $config) ); return static::fieldWrap($name, $input, $config); } protected static function renderTextarea(string $name, mixed $value, array $config): string { $rows = $config['rows'] ?? 5; $quill = (array_key_exists('quill', $config) && $config['quill'] === true) ? ' data-editor="true"' : ''; if ($quill !== '') { $allowImages = array_key_exists('allowImage', $config); $quill .= ($allowImages) ? ' data-allowimage="true"' : ' data-allowimage="false"'; } $textarea = sprintf( '', $rows, static::inputAttrs($name, $config), $quill, esc_textarea($value) ); return static::fieldWrap($name, $textarea, $config); } protected static function renderNumber(string $name, mixed $value, array $config): string { $attrs = static::inputAttrs($name, $config); $value = ($value !== '') ? ' value="'.esc_attr($value).'"' : ''; $input = sprintf( '', $value, $attrs ); return static::fieldWrap($name, $input, $config); } protected static function renderQuantity(string $name, mixed $value, array $config): string { if (!array_key_exists('class', $config)) { $config['class']=[]; } $config['class'][] ='quantity-input'; $attrs = static::inputAttrs($name, $config); $value = ($value !== '') ? ' value="'.esc_attr($value).'"' : ''; $input = sprintf( '
', array_key_exists('remove', $config) ? $config['remove'] : 'Decrease amount', array_key_exists('label', $config) ? $config['label'] : 'Amount', jvbIcon('minus-square'), $value, $attrs, array_key_exists('add', $config) ? $config['add'] : 'Increase amount', array_key_exists('label', $config) ? $config['label'] : 'Amount', jvbIcon('plus-square'), ); return static::fieldWrap($name, $input, $config); } protected static function renderEmail(string $name, mixed $value, array $config): string { $config['validate'] = 'email'; $value = ($value !== '') ? ' value="'.esc_attr($value).'"' : ''; $input = sprintf( '', $value, static::inputAttrs($name, $config) ); return static::fieldWrap($name, $input, $config); } protected static function renderUrl(string $name, mixed $value, array $config): string { $config['validate'] = 'url'; $value = ($value !== '') ? ' value="'.esc_attr($value).'"' : ''; $input = sprintf( '', $value, static::inputAttrs($name, $config) ); return static::fieldWrap($name, $input, $config); } protected static function renderTel(string $name, mixed $value, array $config): string { $config['validate'] = 'phone'; $value = ($value !== '') ? ' value="'.esc_attr($value).'"' : ''; $input = sprintf( '', $value, static::inputAttrs($name, $config) ); return static::fieldWrap($name, $input, $config); } protected static function renderDate(string $name, mixed $value, array $config): string { $format = !empty($config['format']) ? $config['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 } } $value = ($value !== '') ? ' value="'.esc_attr($value).'"' : ''; $input = sprintf( '', $value, static::inputAttrs($name, $config) ); return static::fieldWrap($name, $input, $config); } protected static function renderTime(string $name, mixed $value, array $config): string { $value = ($value !== '') ? ' value="'.esc_attr($value).'"' : ''; $input = sprintf( '', $value, static::inputAttrs($name, $config) ); return static::fieldWrap($name, $input, $config); } protected static function renderDatetime(string $name, mixed $value, array $config): string { $value = ($value !== '') ? ' value="'.esc_attr($value).'"' : ''; $input = sprintf( '', $value, static::inputAttrs($name, $config) ); return static::fieldWrap($name, $input, $config); } protected static function renderTrueFalse(string $name, mixed $value, array $config): string { if (!array_key_exists('class', $config)) { $config['class'] = []; } $config['class'][] ='row btw'; $checked = filter_var($value, FILTER_VALIDATE_BOOLEAN); $input = sprintf( '', static::inputAttrs($name, $config), $checked ? ' checked' : '', array_key_exists('required', $config) && $config['required']===true ? '*' : '' ); unset($config['label']); return static::fieldWrap($name, $input, $config); } protected static function renderToggleText($name, $value, $config):string { if (!isset($config['type'])) { $config['type'] = 'toggle-text'; } $input = sprintf( ' ', static::inputAttrs($name, $config), filter_var($value, FILTER_VALIDATE_BOOLEAN) ? ' checked' : '', $name, array_key_exists('before', $config) ? esc_html($config['before']) : '', array_key_exists('off', $config) ? esc_html($config['off']) : 'Off', array_key_exists('on', $config) ? esc_html($config['on']) : 'On', array_key_exists('after', $config) ? esc_html($config['after']) : '', ); return static::fieldWrap($name, $input, $config); } protected static function renderSelect(string $name, mixed $value, array $config): string { $options = $config['options'] ?? []; $optionsHtml = ''; if (empty($config['required'])) { $optionsHtml .= ''; } else { $optionsHtml .= ''; } foreach ($options as $optValue => $optLabel) { $optionsHtml .= sprintf( '', esc_attr($optValue), selected($value, $optValue, false), esc_html($optLabel) ); } $select = sprintf( '%s', static::inputAttrs($name, $config), $optionsHtml ); return static::fieldWrap($name, $select, $config); } protected static function renderCheckbox(string $name, mixed $value, array $config): string { $options = $config['options'] ?? []; $values = is_array($value) ? $value : explode(',', (string)$value); $values = array_map('trim', $values); $checkboxes = sprintf( '
%s%s', esc_html($config['label'] ?? 'Select Option(s)'), array_key_exists('required', $config) && $config['required']===true ? '*' : '' ); foreach ($options as $optValue => $optLabel) { $checked = in_array($optValue, $values) ? ' checked' : ''; $checkboxes .= sprintf( ' ', esc_attr($name), esc_attr($name), $optValue, esc_attr($optValue), $checked, esc_attr($name), $optValue, esc_html($optLabel) ); } $checkboxes .= '
'; unset($config['label']); return static::fieldWrap($name, $checkboxes, $config); } protected static function renderRadio(string $name, mixed $value, array $config): string { $options = $config['options'] ?? []; $inputClass = !empty($config['inputClass']) ? ' class="' . esc_attr($config['inputClass']) . '"' : ''; $idPrefix = $config['idPrefix'] ?? ''; $radios = sprintf( '
%s%s', array_key_exists('label', $config) ? esc_html($config['label']) : 'Select an option', array_key_exists('required', $config) && $config['required'] === true ? '*' : '' ); foreach ($options as $optValue => $optConfig) { if (is_array($optConfig)) { $optLabel = $optConfig['label'] ?? $optValue; $optIcon = $optConfig['icon'] ?? null; $optDisabled = !empty($optConfig['disabled']) ? ' disabled' : ''; } else { $optLabel = $optConfig; $optIcon = null; $optDisabled = ''; } $labelContent = $optIcon ? jvbDashIcon($optIcon) : '' . esc_html($optLabel) . ''; $optId = esc_attr($idPrefix . $name . '-' . $optValue); $radios .= sprintf( ' ', esc_attr($name), $optId, esc_attr($optValue), checked($value, $optValue, false), $optDisabled, $inputClass, $optId, esc_html($optLabel), $labelContent ); } $radios .= '
'; unset($config['label']); return static::fieldWrap($name, $radios, $config); } protected static function renderUpload(string $name, mixed $value, array $config): string { $defaults = [ //File Type 'subtype' => 'image', //'image', 'video', 'document', 'any' 'accepted' => null, //null = use subtype defaults, or define an array of specific MIME types //Upload Behavious 'multiple' => false, //single or multiple uploads 'limit' => 15, //Max number of uploads (0 = unlimited) 'mode' => 'direct', // 'direct' or 'selection' TODO: unneeded? 'destination'=> 'meta', //'meta', 'post', 'post_group' //Processing Options 'max_size' => null, //override default size limits 'convert' => 'webp', //Image conversion format 'quality' => 90, //Conversion quality 'inputData' => [] ]; $config = array_merge($defaults, $config); // 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 ''; } $validate = [ 'subtype' => ['image', 'video', 'document', 'any'], 'mode' => ['direct', 'selection'], 'destination'=> ['meta', 'post', 'post_group'] ]; foreach ($validate as $key => $options) { if (!in_array($config[$key], $options)) { error_log('Invalid option set for '.$key.': '.print_r($config[$key], true)); return ''; } } $acceptAttr = implode(',',static::getAllowedTypes($config)); //Add upload config to the datasets (handled by fieldWrap()) $attrs = ['subtype', 'mode', 'destination', 'max_size']; foreach ($attrs as $attr) { $config['data'][$attr] = $config[$attr]; } $config['data']['upload-field'] = ''; $config['data']['type'] = $config['multiple'] ? 'gallery' : 'single'; $plural = 'images'; $singular = 'image'; if (!empty($config['content'])) { $content = $config['content']; $config['data']['content'] = $content; $plural = JVB_CONTENT[$content]['plural'] ?? JVB_TAXONOMY[$content]['plural'] ?? JVB_USER[$content]['plural'] ?? str_replace('_', ' ', $content) . 's'; $singular = JVB_CONTENT[$content]['singular'] ?? JVB_TAXONOMY[$content]['singular'] ?? JVB_USER[$content]['singular'] ?? str_replace('_', ' ',$config['content']); } if ($config['limit'] > 0) { $config['data']['max-files'] = $config['limit']; } $attachmentIds = static::parseIds($value); $input = sprintf( '

%s

%s

Click to upload or drag and drop
%s (max. %s)

', static::inputAttrs($name, $config), $acceptAttr, esc_attr(static::getMaxFileSize($config)), array_key_exists('label', $config) ? esc_html($config['label']) : 'Upload file(s)', static::buildDescription($name, $config), esc_html(static::getAcceptedTypesLabel($config)), esc_html(static::formatFileSize(static::getMaxFileSize($config))) ); if ($config['destination'] === 'post_group') { $input .= sprintf( '

You can group images to create separate %s.

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

', $plural, $singular, jvbIcon('star') ); } if (array_key_exists('upload_description', $config) && $config['upload_description']!==''){ $input .= sprintf('

%s

', esc_html($config['upload_description'])); } $input .= '
'; $input .= jvbRenderProgressBar('', false, true, true); $input .= '
'; // closes .file-upload-wrapper if ($config['destination'] === 'post_group') { $input .= static::renderUploadGroupAreaStart($config, $plural, $singular); } $input .= static::renderUploadItem($attachmentIds, $config['subtype']); if ($config['destination'] === 'post_group') { $input .= static::renderUploadGroupAreaEnd($config, $plural, $singular); } $input .= '
'; // closes .file-upload-container unset($config['description']); unset($config['label']); return static::fieldWrap($name, $input, $config); } protected static function renderUploadGroupAreaStart(array $config, string $plural='', string $singular = ''):string { return sprintf('', jvbIcon('arrow-elbow-left-up'), $plural, jvbIcon('arrow-elbow-right-up'), $plural, $plural, $singular, jvbIcon('arrow-elbow-left-up'), $singular, jvbIcon('arrow-elbow-left-up') ); } protected static function getAllowedTypes(array $config): array { if (!empty($config['accepted'])) { return $config['accepted']; } $defaults = [ 'image' => ['image/*'], 'video' => ['video/*'], 'document' => ['application/pdf', 'application/msword', 'application/vnd.ms-excel', 'text/plain', '.odt','application/vnd.openxmlformats-officedocument.wordprocessingml.document'], ]; $defaults['any'] = array_merge(array_values($defaults)); return $defaults[$config['subtype']]??$defaults['image']; } protected static function getMaxFileSize(array $config):string { $defaults = [ 'image' => 5242880, // 5MB 'video' => 104857600, // 100MB 'document' => 10485760 // 10MB ]; return absint($config['max_size']??$defaults[$config['subtype']] ?? $defaults['image']); } protected static 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'; } protected static function getAcceptedTypesLabel(array $config):string { $labels = [ 'image' => 'JPG, JPEG, PNG, GIF, or WEBP', 'video' => 'MP4, WEBM, or MOV', 'document'=> 'PDF, DOC, XLS, or TXT', 'any' => 'Images, Videos, or Documents' ]; return $labels[$config['subtype']] ?? strtoupper(implode(', ', array_map(function($ext) { return ltrim($ext, '.'); }, array_slice($config['accepted'], 0, 3)))); } protected static function parseIds(mixed $value):array { if (empty($value)) { return []; } if (is_string($value)) { $value = explode(',',$value); } return array_filter(array_map('absint', $value), function($item) { return $item > 0; }); } protected static function renderGallery(string $name, mixed $value, array $config): string { $config['multiple'] = true; return static::renderUpload($name,$value,$config); } protected static function renderSelector(string $name, mixed $value, array $config, string $extra =''):string { $ids = static::parseIds($value); if (!array_key_exists('subtype', $config) || !in_array($config['subtype'], ['taxonomy', 'content', 'user'])){ error_log('Invalid subtype for Selector: '.print_r($config['subtype']??false, true)); return ''; } $config = array_merge([ 'max' => $config['max']??0, 'search' => $config['search']??true, 'createNew' => $config['createNew']??false, 'autocomplete'=> $config['autocomplete']??true, 'name' => $name, 'update' => $config['update']??true, 'required' => $config['required']??false, 'type' => $config['subtype'], ], $config); $icon = match ($config['subtype']) { 'taxonomy' => JVB_TAXONOMY[$config['taxonomy']]['icon'] ?? jvbDefaultIcon(), 'content' => JVB_CONTENT[$config['content']]['icon'] ?? jvbDefaultIcon(), 'user' => JVB_USER[$config['role']]['icon'] ?? 'user', default => jvbDefaultIcon(), }; $containerId = sprintf('%s-%s-selector', $name, $config['subtype']); $input = sprintf( '
', esc_attr($name), jvbIcon($icon), esc_html($config['label']), ); $input .= static::buildSelectorButton($ids, $config); if ($config['autocomplete']) { $input .= static::buildSelectorAutocomplete($name, $config); } $plural = static::getPlural($config); $input .= sprintf( '
', $plural[1]??'' ); $config['type'] = 'selector'; unset($config['label']); unset($config['description']); unset($config['hint']); $config['skipInput'] = true; return static::fieldWrap($containerId, $input, $config); } protected static function getPlural(array $config):array { switch ($config['subtype']) { case 'taxonomy': if (array_key_exists($config['taxonomy'], JVB_TAXONOMY)) { $single = JVB_TAXONOMY[$config['taxonomy']]['singular']; $plural = JVB_TAXONOMY[$config['taxonomy']]['plural']; } else { $taxonomy = get_taxonomy($config['taxonomy']); if (!$taxonomy) { return []; } $single = $taxonomy->labels->singular_name; $plural = $taxonomy->labels->name; } break; case 'content': if (array_key_exists($config['content'], JVB_CONTENT)) { $single = JVB_CONTENT[$config['content']]['singular']; $plural = JVB_CONTENT[$config['content']]['plural']; } else { $postType = get_post_type_object($config['content']); if (!$postType) { return ''; } $single = $postType->labels->singular_name; $plural = $postType->labels->name; } break; case 'user': if (array_key_exists($config['user'], JVB_USER)) { $single = JVB_USER[$config['user']]['singular']; $plural = JVB_USER[$config['user']]['plural']; } else { $user = get_role($config['user']); if (!$user) { return ''; } $single = 'User'; $plural = 'Users'; } break; } return [$single, $plural]; } protected static function buildSelectorButton(array $ids, array $config):string { switch ($config['subtype']) { case 'taxonomy': if (array_key_exists($config['taxonomy'], JVB_TAXONOMY)) { $single = JVB_TAXONOMY[$config['taxonomy']]['singular']; $plural = JVB_TAXONOMY[$config['taxonomy']]['plural']; } else { $taxonomy = get_taxonomy($config['taxonomy']); if (!$taxonomy) { return ''; } $single = $taxonomy->labels->singular_name; $plural = $taxonomy->labels->name; } $attr = sprintf( ' data-taxonomy="%s" data-single="%s" data-plural="%s"', $config['taxonomy'], $single, $plural ); break; case 'content': if (array_key_exists($config['content'], JVB_CONTENT)) { $single = JVB_CONTENT[$config['content']]['singular']; $plural = JVB_CONTENT[$config['content']]['plural']; } else { $postType = get_post_type_object($config['content']); if (!$postType) { return ''; } $single = $postType->labels->singular_name; $plural = $postType->labels->name; } $attr = sprintf( ' data-content="%s" data-single="%s" data-plural="%s"', $config['content'], $single, $plural ); break; case 'user': if (array_key_exists($config['user'], JVB_USER)) { $single = JVB_USER[$config['user']]['singular']; $plural = JVB_USER[$config['user']]['plural']; } else { $user = get_role($config['user']); if (!$user) { return ''; } $single = 'User'; $plural = 'Users'; } $attr = sprintf( ' data-user="%s" data-single="%s" data-plural="%s"', $config['user'], $single, $plural ); break; } $dataAttrs = []; if ($config['update']) { $dataAttrs[] = 'data-update="false"'; } if ($config['max']>0) { $dataAttrs[] = 'data-max="'.esc_attr($config['max']).'"'; } if ($config['search']) { $dataAttrs[] = 'data-search'; } if ($config['createNew']) { $dataAttrs[] = 'data-creatable'; } if (array_key_exists('types', $config) && is_array($config['types'])) { $dataAttrs[] = 'data-for="'.esc_attr(implode(',',$config['types'])).'"'; } if (!empty($selected)) { $dataAttrs[] = 'data-selected="'.esc_attr(implode(',',$selected)).'"'; } if ($config['autocomplete']) { $dataAttrs[] = 'data-autocomplete'; } if (array_key_exists('hidden', $config) && $config['hidden']) { $dataAttrs[] = 'hidden'; } $dataAttrs = implode(' ',$dataAttrs); return sprintf( '', $attr, $dataAttrs, $single, $plural, jvbIcon('plus-square') ); } protected static function buildSelectorAutocomplete(string $name, array $config): string { $containerId = sprintf('%s-%s-selector', $name, $config['subtype'] ?? $config['type']); return sprintf( ' ', esc_attr($name), esc_attr($name) ); } protected static function renderTaxonomy(string $name, mixed $value, array $config): string { $config['subtype'] = 'taxonomy'; return static::renderSelector($name, $value, $config); } protected static function renderUser(string $name, mixed $value, array $config): string { $config['subtype'] = 'user'; return static::renderSelector($name, $value, $config); } protected static function renderContent(string $name, mixed $value, array $config): string { $config['subtype'] = 'content'; return static::renderSelector($name, $value, $config); } protected static function renderLocation(string $name, mixed $value, array $config): string { $googleMaps = JVB()->connect('maps'); if (!$googleMaps->isSetUp()) { return '

Google Maps not configured. Please configure in Integrations settings.

'; } $field_id = esc_attr($name); $map_id = sprintf('%s_map', $field_id); $components = ['address', 'lat', 'lng', 'street', 'city', 'province', 'postal_code', 'country']; if (!empty($value)) { $lat = (float)$value['lat']??''; $lng = (float)$value['lng']??''; $coords = [ 'lat' => $lat, 'ng' => $lng ]; } else { $coords = null; } if (!array_key_exists('data', $config)) { $config['data'] =[]; } $js_config = [ 'fieldId' => $field_id, 'initialCoords' => $coords ]; $json_config = htmlspecialchars(json_encode($js_config), ENT_QUOTES, 'UTF-8'); $config['data']['location-field-init'] = $json_config; $input = ''; if (!empty($value) && array_key_exists('address', $value)) { $input = sprintf( '

Current location: %s

Search below to change:

', esc_html($value['address']) ); } $links = (!empty($value)) ? jvbLocationLinks($value) : ''; $input .= sprintf( '
%s
', esc_attr($map_id), $links ); if (!empty($value)) { foreach($components as $el) { $input .= sprintf( '', esc_attr($name), $el, $value[$el]??'', $el ); } } $input .= '
'; return static::fieldWrap($name, $input, $config); } protected static function renderTagList(string $name, mixed $value, array $config):string { $tagFormat = $config['tag_format']??'first_field'; if (!array_key_exists('data', $config)) { $config['data']= []; } $config['data']['tag-format'] = esc_attr($tagFormat); $input = sprintf( '

%s

', esc_html($config['label']??'') ); foreach ($config['fields'] as $fieldName => $fieldConfig) { $newName = sprintf('new_%s', $fieldName); if (array_key_exists('required', $fieldConfig)) { $fieldConfig['data']['required'] = true; unset($fieldConfig['required']); } $fieldConfig['data']['ignore'] = true; $input .= static::render($newName, '', $fieldConfig); } $input .= sprintf( '
', jvbIcon('plus'), $config['add_label']??'Add' ); //Tag Display $input .= '
'.static::renderTagItems($config['fields'], $value, $name, $tagFormat).'
'; //Template for tags $input .= sprintf( '', uniqid('tagListItem'), static::renderTagItem($config['fields'], [], $name, null, $tagFormat) ); $input .= '
'; unset($config['label']); return static::fieldWrap($name, $input, $config); } protected static function renderTagItems(array $fields, mixed $value, string $name, string $tagFormat):string { if (!$value || $value === '') { return ''; } if (is_string($value)) { $value = explode(',', $value); } if (empty($value)) { return ''; } $out = ''; foreach ($value as $index => $v) { $out .= static::renderTagItem($fields, $v, $name, $index, $tagFormat); } return $out; } protected static function renderTagItem(array $fields, mixed $values, string $name, ?int $index, string $tagFormat):string { $tagText = static::getTagDisplayText($fields, $values, $tagFormat); $out = sprintf( '
%s', ($index !== null) ? ' data-index="'.$index.'"' : '', $tagText ); foreach ($fields as $fieldName => $fieldConfig) { $value = $values[$fieldName]??''; $fullName = ($index === null) ? $fieldName : sprintf('%s:%s:%s', $name, $index, $fieldName); $out .= sprintf( '', esc_attr($fullName), esc_attr($value), esc_attr($fieldName), esc_attr($fieldConfig['type']), esc_attr($fullName) ); } $out .= sprintf( '', jvbIcon('x') ); $out .='
'; return $out; } protected static function getTagDisplayText(array $fields, mixed $values, string $tagFormat):string { if (empty($values)) { return 'New Item'; } switch ($tagFormat) { case 'first_field': $firstKey = array_key_first($fields); return $values[$firstKey] ?? 'New Item'; case 'all_fields': $values = array_filter(array_values($values)); return implode(', ', $values) ?: 'New Item'; default: if (strpos($tagFormat, '{') !== false) { $text = $tagFormat; foreach ($values as $key => $value) { $text = str_replace('{'.$key.'}', $value, $text); } return $text; } return $values[$tagFormat]??'New Item'; } } protected static function renderRepeater(string $name, mixed $value, array $config): string { $fields = $config['fields'] ?? []; $rows = is_array($value) ? $value : []; if(array_key_exists('row_label', $config)) { $config['data']['label'] = esc_attr($config['row_label']); } $input = sprintf( '

%s

', esc_html($config['label'] ?? '') ); $input .= '
'; $rowTitle = array_key_exists('new_row', $config) ? $config['new_row'] : 'New Item'; if(!empty($rows)) { foreach ($rows as $index=>$row) { $input .= static::renderRepeaterRow($config['fields'], $row, $index, $name, $rowTitle); } } $input .= '
'; $input .= ''; $input .= sprintf( '', jvbIcon('plus', ['title'=>'Add']), array_key_exists('add_label', $config) ? $config['add_label'] : 'Add Item' ); return static::fieldWrap($name, $input, $config); } protected static function renderRepeaterRow(array $fields, array $values, int|string $index, string $name, string $rowTitle='New Item'):string { $displayNumber = (is_string($index)) ? $index : ($index +1); $output = sprintf( '
%s #%s %s
', esc_attr($index), jvbIcon('trash'), is_string($index) ? ' open' : '', jvbIcon('dots-six-vertical'), $displayNumber, $rowTitle ); foreach ($fields as $fieldName => $fieldConfig) { $fieldValue = $values[$fieldName] ?? ''; $fullName = ($name === '') ? $fieldName : sprintf('%s:%s:%s', $name, $index, $fieldName); $output .= static::render($fullName, $fieldValue, $fieldConfig); } return $output.'
'; } /** * Group fields are mainly for ease of conditional logic and visual layout * @param string $name * @param mixed $value * @param array $config * @return string */ protected static function renderGroup(string $name, mixed $value, array $config): string { $fields = $config['fields'] ?? []; $values = is_array($value) ? $value : []; $wrapper = (array_key_exists('wrap', $config)) ? 'details' : 'fieldset'; $legend = (array_key_exists('wrap', $config)) ? 'summary' : 'legend'; $output = sprintf( '<%s><%s>%s' , esc_attr($wrapper), esc_attr($legend), array_key_exists('label', $config) ? $config['label'] : 'Group', esc_attr($legend) ); foreach ($fields as $fieldName => $fieldConfig) { $fieldValue = $values[$fieldName] ?? ''; $fullName = array_key_exists('wrap', $config) ? $fieldName : "{$name}:{$fieldName}"; $output .= static::render($fullName, $fieldValue, $fieldConfig); } $output .= sprintf('', esc_attr($wrapper)); unset($config['label']); return static::fieldWrap($name, $output, $config); } public static function outputSelectorModal():string { return sprintf('
%s
', static::search('Search terms', 'search-terms'), jvbModalActions(), jvbIcon('plus-square'), jvbIcon('x') ); } public static function search(string $placeholder = 'Search...', string $id = 'search'):string { $id = sanitize_title($id); return sprintf( '
', $id, $placeholder, jvbIcon('x', ['title' => 'Clear Search']), jvbIcon('magnifying-glass') ); } }