'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 = '
',
$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(
'
%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('plus-square'),
jvbIcon('trash'),
jvbIcon('cloud-arrow-up'),
$plural!==''? $plural : $config['content'],
);
}
protected static function renderUploadItem(array $attachmentIds, string $subtype):string
{
$out = jvbRenderProgressBar('
Processing files...
0/0',false,true,true);
$out .= '
';
// Render existing attachments
foreach ($attachmentIds as $attachmentId) {
$out .= static::renderExistingAttachment($attachmentId, $subtype);
}
$out .= '
';
return $out;
}
public static function renderExistingAttachment(int $attachmentId, string $subtype):string
{
switch ($subtype){
case 'video':
return static::renderVideoPreview($attachmentId);
case 'document':
case 'file':
return static::renderFilePreview($attachmentId);
default:
return static::renderImagePreview($attachmentId);
}
}
protected static function renderUploadItemStart(?int $attachmentId = null):string
{
return sprintf(
'
';
}
protected static function renderUploadItemMetaEnd():string
{
return '
';
}
protected static function renderVideoPreview(?int $ID = null, ?array $additionalFields = null):string
{
$out = static::renderUploadItemStart($ID);
//add video preview
$previewID = get_post_meta($ID, BASE.'poster', true);
if ($previewID !== '') {
$out .= jvbFormatImage($previewID, 'tiny', 'medium');
} else {
$out .= '
![]()
';
}
$out .= static::renderUploadItemEnd();
//add item actions
$out .= static::renderUploadItemActions($ID);
$out .= static::renderUploadItemMetaStart();
//Caption, description, title
$caption = ($ID) ? wp_get_attachment_caption($ID) : '';
$description = ($ID) ? get_the_content($ID) : '';
$title = ($ID) ? get_the_title($ID) : '';
$fields = [
'type' => 'group',
'wrap' => 'details',
'label' => 'Edit Video Meta',
'fields' => [
'image-title' => [
'type' => 'text',
'label' => 'Video Title',
'value' => $title,
'data' => ['id' => $ID]
],
'poster' => [
'type' => 'upload',
'label' => 'Video Poster',
'value' => $previewID,
'multiple' => false,
],
'image-caption' => [
'type' => 'textarea',
'value' => $caption,
'label' => 'Video Caption',
'data' => ['id' => $ID]
],
'image-description' => [
'type' => 'textarea',
'value' => $description,
'label' => 'Video Description',
'data' => ['id' => $ID]
]
]
];
$out .= static::render('image_data', '', $fields);
$out .= static::renderUploadItemMetaEnd();
if ($additionalFields) {
$out .= static::additionalFields($additionalFields);
}
return $out;
}
protected static function renderFilePreview(?int $ID, ?array $additionalFields = null):string
{
$out = static::renderUploadItemStart($ID);
$upload = wp_get_attachment_url($ID);
$fileType = wp_check_filetype($upload)['ext']??false;
$iconMap = [
'pdf' => 'file-pdf',
'csv' => 'file-csv',
'doc' => 'file-doc',
'docx' => 'file-doc',
'txt' => 'file-txt',
'xls' => 'file-xls',
'xlsx' =>'file-xls'
];
$icon = ($fileType) ? jvbIcon($iconMap[$fileType]??'file') : jvbIcon('file');
$out .= '
'.$icon.'';
$out .= static::renderUploadItemEnd();
//add item actions
$out .= static::renderUploadItemActions($ID);
$out .= static::renderUploadItemMetaStart();
//Caption, description, title
$caption = ($ID) ? wp_get_attachment_caption($ID) : '';
$description = ($ID) ? get_the_content($ID) : '';
$title = ($ID) ? get_the_title($ID) : '';
$fields = [
'type' => 'group',
'wrap' => 'details',
'label' => 'Edit File Meta',
'fields' => [
'image-title' => [
'type' => 'text',
'label' => 'File Title',
'value' => $title,
'data' => ['id' => $ID]
],
'poster' => [
'type' => 'upload',
'label' => 'File Poster',
'multiple' => false,
],
'image-caption' => [
'type' => 'textarea',
'value' => $caption,
'label' => 'File Caption',
'data' => ['id' => $ID]
],
'image-description' => [
'type' => 'textarea',
'value' => $description,
'label' => 'File Description',
'data' => ['id' => $ID]
]
]
];
$out .= static::render('image_data', '', $fields);
$out .= static::renderUploadItemMetaEnd();
if ($additionalFields) {
$out .= static::additionalFields($additionalFields);
}
return $out;
}
public static function renderImagePreview(?int $ID = null, ?array $additionalFields = null):string
{
$out = static::renderUploadItemStart($ID);
//add image preview
if ($ID) {
$out .= jvbFormatImage($ID, 'tiny', 'medium');
} else {
$out .= '
![]()
';
}
$out .= static::renderUploadItemEnd();
//add item actions
$out .= static::renderUploadItemActions($ID);
$out .= static::renderUploadItemMetaStart();
//Caption, description, title
$caption = ($ID) ? wp_get_attachment_caption($ID) : '';
$description = ($ID) ? get_the_content($ID) : '';
$alt = ($ID) ? get_post_meta($ID, '_wp_attachment_image_alt', true) : '';
$title = ($ID) ? get_the_title($ID) : '';
$fields = [
'type' => 'group',
'wrap' => 'details',
'label' => 'Edit Image Meta',
'fields' => [
'image-title' => [
'type' => 'text',
'label' => 'Image Title',
'value' => $title,
'data' => ['id' => $ID]
],
'image-alt-text' => [
'type' => 'text',
'label' => 'Alt Text',
'value' => $alt,
'hint' => 'Alt text helps the visually impaired, as well as some benefits for SEO.',
'data' => ['id' => $ID]
],
'image-caption' => [
'type' => 'textarea',
'value' => $caption,
'label' => 'Image Caption',
'data' => ['id' => $ID]
],
'image-description' => [
'type' => 'textarea',
'value' => $description,
'label' => 'Image Description',
'data' => ['id' => $ID]
]
]
];
$out .= static::render('image_data', '', $fields);
$out .= static::renderUploadItemMetaEnd();
if ($additionalFields) {
$out .= static::additionalFields($additionalFields);
}
return $out;
}
protected static function additionalFields(array $fields):string
{
$out = '';
foreach ($fields as $name => $config) {
$out .= static::render($name, '', $config);
}
return $out;
}
protected static function renderUploadGroupAreaEnd(array $config, string $plural, string $singular):string
{
return sprintf(
'
%s These will become individual %s %s
',
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[] = '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
{
return sprintf(
'
{ Loading items }
',
$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(
'
',
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(
'
%s',
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(
'';
$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 .= '