<?php
|
namespace JVBase\meta;
|
|
use DateTime;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Static utility for rendering form fields
|
*
|
* Usage:
|
* echo Form::render('price', 150, ['type' => '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 = '<form id="' . esc_attr($endpoint) . '"' . $classes . ' data-save="' . esc_attr($endpoint) . '" data-form-id="' . esc_attr($id) . '">';
|
|
if (!empty($options['heading'])) {
|
$output .= '<h2>' . esc_html($options['heading']) . '</h2>';
|
}
|
|
if (!empty($options['description'])) {
|
$descriptions = is_array($options['description']) ? $options['description'] : [$options['description']];
|
foreach ($descriptions as $d) {
|
$output .= '<p>' . esc_html($d) . '</p>';
|
}
|
}
|
|
foreach ($meta->configs() as $name => $config) {
|
$output .= static::render($name, $meta->get($name), $config);
|
}
|
|
if (!empty($options['submit'])) {
|
$output .= '<button type="submit">' . jvbIcon('floppy-disk') . 'Save</button>';
|
}
|
|
$output .= '</form>';
|
|
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(
|
'<div class="%s" data-field="%s" data-field-type="%s"%s>',
|
$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 .= '</div>';
|
|
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(
|
'<span class="char-limit"><span class="current">0</span> / <span class="limit">%s</span></span>',
|
esc_html($config['maxlength'])
|
);
|
}
|
|
protected static function buildLabel(string $name, array $config):string
|
{
|
if (!empty($config['label'])) {
|
return sprintf(
|
'<label for="%s">%s%s</label>',
|
esc_attr($name),
|
esc_html($config['label']),
|
!empty($config['required']) ? '<span class="required" aria-label="required">*</span>' : ''
|
);
|
}
|
return '';
|
}
|
|
public static function buildInput(string $content):string
|
{
|
return sprintf(
|
'<div class="field-input-wrapper">
|
%s
|
<span class="validation-icon success" hidden aria-hidden="true">%s</span>
|
<span class="validation-icon error" hidden aria-hidden="true">%s</span>
|
</div><span class="validation-message" hidden role="alert"></span>',
|
$content,
|
jvbIcon('check-circle'),
|
jvbIcon('x-circle')
|
);
|
}
|
|
protected static function buildHint(array $config):string
|
{
|
if (!empty($config['hint'])) {
|
return sprintf(
|
'<span class="hint">%s</span>',
|
esc_html($config['hint'])
|
);
|
}
|
return '';
|
}
|
|
protected static function buildDescription(string $name, array $config):string
|
{
|
if (!empty($config['description'])) {
|
return sprintf(
|
'<p class="description" id="%s">%s</p>',
|
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(
|
'<input type="%s"%s%s />',
|
$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(
|
'<textarea rows="%d"%s%s>%s</textarea>',
|
$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(
|
'<input type="number"%s%s />',
|
$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(
|
'<div class="quantity">
|
<button type="button" class="decrease" title="%s" aria-label="Decrease %s">%s</button>
|
<input type="number"%s%s />
|
<button type="button" class="increase" title="%s" aria-label="Increase %s">%s</button>
|
</div>',
|
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(
|
'<input type="email"%s%s />',
|
$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(
|
'<input type="url"%s%s />',
|
$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(
|
'<input type="tel"%s%s />',
|
$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(
|
'<input type="date"%s%s />',
|
$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(
|
'<input type="time"%s%s />',
|
$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(
|
'<input type="datetime-local"%s%s />',
|
$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(
|
'<label class="toggle-switch row">
|
<input type="checkbox" value="1"%s%s />
|
<div class="slider"></div>
|
<span class="toggle-label">%s</span>
|
</label>',
|
static::inputAttrs($name, $config),
|
$checked ? ' checked' : '',
|
array_key_exists('required', $config) && $config['required']===true ? '<span class="required" aria-label="required">*</span>' : ''
|
);
|
|
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(
|
'<input type="checkbox" value="all"%s%s>
|
<label for="%s" class="row">
|
%s
|
<span class="text row">
|
<span class="off">%s</span>
|
<span class="on">%s</span>
|
</span>
|
%s
|
</label>',
|
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 .= '<option value="">— Select —</option>';
|
} else {
|
$optionsHtml .= '<option value="" disabled selected hidden>— Select —</option>';
|
}
|
|
foreach ($options as $optValue => $optLabel) {
|
$optionsHtml .= sprintf(
|
'<option value="%s"%s>%s</option>',
|
esc_attr($optValue),
|
selected($value, $optValue, false),
|
esc_html($optLabel)
|
);
|
}
|
|
$select = sprintf(
|
'<select%s>%s</select>',
|
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(
|
'<fieldset>
|
<legend>%s%s</legend>',
|
esc_html($config['label'] ?? 'Select Option(s)'),
|
array_key_exists('required', $config) && $config['required']===true ? '<span class="required" aria-label="required">*</span>' : ''
|
);
|
|
foreach ($options as $optValue => $optLabel) {
|
$checked = in_array($optValue, $values) ? ' checked' : '';
|
$checkboxes .= sprintf(
|
'
|
<input type="checkbox" name="%s[]" id="%s-%s" value="%s"%s />
|
<label class="checkbox-option" for="%s-%s">
|
<span>%s</span>
|
</label>',
|
esc_attr($name),
|
esc_attr($name),
|
$optValue,
|
esc_attr($optValue),
|
$checked,
|
esc_attr($name),
|
$optValue,
|
esc_html($optLabel)
|
);
|
}
|
|
$checkboxes .= '</fieldset>';
|
|
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(
|
'<fieldset>
|
<legend>%s%s</legend>',
|
array_key_exists('label', $config) ? esc_html($config['label']) : 'Select an option',
|
array_key_exists('required', $config) && $config['required'] === true
|
? '<span class="required" aria-label="required">*</span>' : ''
|
);
|
|
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)
|
: '<span>' . esc_html($optLabel) . '</span>';
|
|
$optId = esc_attr($idPrefix . $name . '-' . $optValue);
|
|
$radios .= sprintf(
|
'<input type="radio" name="%s" id="%s" value="%s"%s%s%s />
|
<label class="radio-option" for="%s" title="%s">%s</label>',
|
esc_attr($name),
|
$optId,
|
esc_attr($optValue),
|
checked($value, $optValue, false),
|
$optDisabled,
|
$inputClass,
|
$optId,
|
esc_html($optLabel),
|
$labelContent
|
);
|
}
|
|
$radios .= '</fieldset>';
|
|
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(
|
'<div class="file-upload-container">
|
<div class="file-upload-wrapper">
|
<input type="file"
|
%s
|
accept="%s"
|
data-max-size="%s"
|
data-ignore>
|
<h2>%s</h2>
|
%s
|
<p class="file-upload-text">
|
<strong>Click to upload</strong> or drag and drop<br>
|
%s
|
(max. %s)
|
</p>',
|
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(
|
'<p class="hint">You can group images to create separate %s.</p>
|
<p class="hint">If a %s has multiple images, you can select the %s to set an image as the main one.</p>',
|
$plural,
|
$singular,
|
jvbIcon('star')
|
);
|
}
|
|
if (array_key_exists('upload_description', $config) && $config['upload_description']!==''){
|
$input .= sprintf('<p>%s</p>', esc_html($config['upload_description']));
|
}
|
|
$input .= '<div class="file-error"></div>';
|
$input .= jvbRenderProgressBar('', false, true, true);
|
|
$input .= '</div>'; // 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 .= '</div>'; // 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('<div class="group-display flex col" hidden>
|
<div class="preview-wrap flex col">
|
<div class="preview-actions">
|
<div class="selection-controls">
|
<div class="selected">
|
<div class="field">
|
<input type="checkbox" id="select-all-uploads" data-select-all data-selects="item-grid" name="select-all-uploads">
|
<label for="select-all-uploads">
|
Select All
|
</label>
|
</div>
|
<div class="info" hidden>
|
|
</div>
|
</div>
|
|
<div class="selection-actions row btw" hidden>
|
<button type="button" data-action="add-to-group">
|
%sGroup
|
</button>
|
<button type="button" data-action="delete-upload">
|
%sDelete
|
</button>
|
</div>
|
</div>
|
|
<button type="button" data-action="upload" class="submit-uploads">
|
%sUpload %s
|
</button>
|
</div>',
|
jvbIcon('plus-square'),
|
jvbIcon('trash'),
|
jvbIcon('cloud-arrow-up'),
|
$plural!==''? $plural : $config['content'],
|
);
|
}
|
|
protected static function renderUploadItem(array $attachmentIds, string $subtype):string
|
{
|
$out = jvbRenderProgressBar('<span class="text">Processing files...</span>
|
<span class="count">0/0</span>',false,true,true);
|
$out .= '<div class="item-grid preview">';
|
// Render existing attachments
|
foreach ($attachmentIds as $attachmentId) {
|
$out .= static::renderExistingAttachment($attachmentId, $subtype);
|
}
|
$out .= '</div>';
|
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(
|
'<div class="item upload" data-id="%d">
|
<div class="preview">
|
<input type="checkbox" class="select-item" name="select-item" id="select-item%d">
|
<label for="select-item%d" aria-label="Select image">',
|
$attachmentId,
|
($attachmentId) ? '-'.$attachmentId : '',
|
($attachmentId) ? '-'.$attachmentId : ''
|
);
|
}
|
protected static function renderUploadItemEnd():string {
|
return '</label>';
|
}
|
protected static function renderUploadItemActions(?int $attachmentId = null):string
|
{
|
return sprintf(
|
'<div class="item-actions row btw">
|
<div class="radio-button">
|
<input type="radio" class="featured btn" name="featured" id="featured%d" hidden>
|
<label for="featured">
|
%s%s<span class="screen-reader-text">Set as featured image</span>
|
</label>
|
</div>
|
<button type="button" data-action="delete-upload" title="Remove from Group">
|
%s
|
</button>
|
</div>',
|
($attachmentId) ? '-'.$attachmentId : '',
|
jvbIcon('star'),
|
jvbIcon('star', ['style' => 'fill']),
|
jvbIcon('trash')
|
);
|
}
|
protected static function renderUploadItemMetaStart():string
|
{
|
return '</div>';
|
}
|
protected static function renderUploadItemMetaEnd():string
|
{
|
return '</div>';
|
}
|
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 .= '<img><video></video><span></span>';
|
}
|
$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 .= '<span>'.$icon.'</span>';
|
|
$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 .= '<img><video></video><span></span>';
|
}
|
$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(
|
'<p class="hint">%s These will become individual %s %s</p>
|
</div>
|
<div class="sidebar flex col">
|
<div class="header">
|
<h4>New %s</h4>
|
<p class="hint">Drag or select multiple images into groups to create separate %s.</p>
|
</div>
|
<div class="item-grid groups">
|
<div class="empty-group">
|
<p>Drag here to create a new %s!</p>
|
</div>
|
</div>
|
<p class="hint">%s Each group will become its own %s %s</p>
|
</div>
|
</div>',
|
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(
|
'<div class="row btw">
|
<input type="hidden" name="%s" value="%s">
|
<label for="%s-autocomplete">%s<span>%s</span></label>',
|
esc_attr($name),
|
esc_attr(!empty($ids) ? implode(',', $ids) : ''),
|
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(
|
'<div class="selected-items row" role="region" aria-label="Selected %s"></div>',
|
$plural[1]??''
|
);
|
|
$config['type'] = 'selector';
|
unset($config['label']);
|
unset($config['description']);
|
unset($config['hint']);
|
$config['skipInput'] = true;
|
return static::fieldWrap($name, $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(
|
'<button type="button" class="filter-toggle selector-toggle"%s%s title="Open %s Selector" aria-label="Select %s">%s</button>',
|
$attr,
|
$dataAttrs,
|
$single,
|
$plural,
|
jvbIcon('plus-square')
|
);
|
}
|
protected static function buildSelectorAutocomplete(string $name, array $config): string
|
{
|
return sprintf(
|
'<input type="search" id="%s-autocomplete" autocomplete="off" data-ignore data-autocomplete>
|
<p class="message" hidden aria-live="polite">{ <span>Loading items</span> }</p>
|
<div class="auto-wrapper" hidden><ul class="search-results"></ul><button class="submit-term" hidden data-ignore><strong>Create: </strong> "<span></span>"</button></div>',
|
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 '<div class="notice notice-warning"><p>Google Maps not configured. Please configure in Integrations settings.</p></div>';
|
}
|
|
$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(
|
'<p><b>Current location:</b> %s</p><p class="hint">Search below to change:</p>',
|
esc_html($value['address'])
|
);
|
}
|
$links = (!empty($value)) ? jvbLocationLinks($value) : '';
|
$input .= sprintf(
|
'<div class="location-search-wrapper">
|
<div class="autocomplete-wrapper"></div>
|
<div class="location-preview">
|
<div id="%s" class="location-map"></div>
|
%s
|
</div>',
|
esc_attr($map_id),
|
$links
|
);
|
|
if (!empty($value)) {
|
foreach($components as $el) {
|
$input .= sprintf(
|
'<input type="hidden"
|
name="%s[%s]"
|
value="%s"
|
data-location-field="%s">',
|
esc_attr($name),
|
$el,
|
$value[$el]??'',
|
$el
|
);
|
}
|
}
|
|
$input .= '</div>';
|
|
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(
|
'<h3>%s</h3><div class="row start wrap">',
|
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(
|
'<button type="button" class="button add-tag">%s<span>%s</span></button></div>',
|
jvbIcon('plus'),
|
$config['add_label']??'Add'
|
);
|
|
//Tag Display
|
$input .= '<div class="tag-items">'.static::renderTagItems($config['fields'], $value, $name, $tagFormat).'</div>';
|
|
//Template for tags
|
$input .= sprintf(
|
'<template class="%s">%s</template>',
|
uniqid('tagListItem'),
|
static::renderTagItem($config['fields'], [], $name, null, $tagFormat)
|
);
|
$input .= '</div>';
|
|
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(
|
'<div class="tag-item"%s><span class="tag-label">%s</span>',
|
($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(
|
'<input type="hidden"
|
name="%s"
|
value="%s"
|
data-field="%s"
|
data-field-type="%s"
|
id="%s" />',
|
esc_attr($fullName),
|
esc_attr($value),
|
esc_attr($fieldName),
|
esc_attr($fieldConfig['type']),
|
esc_attr($fullName)
|
);
|
}
|
|
$out .= sprintf(
|
'<button type="button" class="remove-tag" aria-label="Remove">%s</button>',
|
jvbIcon('x')
|
);
|
$out .='</div>';
|
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(
|
'<h3>%s</h3>',
|
esc_html($config['label'] ?? '')
|
);
|
|
$input .= '<div class="repeater-items">';
|
$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 .= '</div>';
|
|
$input .= '<template class="'.uniqid('repeaterRow').'">';
|
$input .= static::renderRepeaterRow($config['fields'], [], '','',$rowTitle);
|
$input .= '</template>';
|
|
$input .= sprintf(
|
'<button type="button" class="add-repeater-row">%s%s</button>',
|
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(
|
'<div class="repeater-row" data-index="%s">
|
<button type="button" class="remove-row" title="Remove">
|
%s
|
</button>
|
<details%s>
|
<summary class="row btw repeater-row-header">
|
<span class="drag-handle">%s</span>
|
<span class="row-number">#%s</span>
|
<span class="row-title">%s</span>
|
</summary>
|
<div class="repeater-row-content">',
|
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.'</div></details></div>';
|
}
|
|
/**
|
* 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</%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('</%s>', esc_attr($wrapper));
|
|
unset($config['label']);
|
return static::fieldWrap($name, $output, $config);
|
}
|
|
public static function outputSelectorModal():string
|
{
|
return sprintf('<dialog id="jvb-selector" aria-labelledby="modal-title" aria-modal="true">
|
<div class="wrap col">
|
<header class="modal-header">
|
<h3 id="modal-title">Select Taxonomy</h3>
|
</header>
|
|
|
<div class="selected-items row" role="region" aria-label="Selected items"></div>
|
|
<div class="items-wrap">
|
<!-- Common/Favorite terms section -->
|
<details class="favourite-terms" hidden>
|
<summary class="title row btw">Your Go Tos:</summary>
|
<ul class="favourite-list row btw"></ul>
|
</details>
|
|
<!-- Pagination info -->
|
<p class="pagination-info" hidden></p>
|
|
<!-- Navigation breadcrumbs -->
|
<nav class="term-navigation row" aria-label="Term navigation">
|
<button type="button" class="back-to-parent" hidden>
|
<span aria-hidden="true">←</span> Back
|
</button>
|
</nav>
|
|
|
<p class="message" hidden aria-live="polite">
|
{ <span>loading items</span> }
|
</p>
|
<!-- Terms list -->
|
<ul class="items-container col start" role="listbox" aria-label="Available terms">
|
<!-- Terms will be populated here -->
|
</ul>
|
|
<button class="submit-term" hidden data-ignore><strong>Create: </strong> "<span></span>"</button>
|
|
<!-- Infinite scroll sentinel -->
|
<div class="scroll-sentinel" aria-hidden="true"></div>
|
</div>
|
|
<!-- Search section -->
|
<div class="search-wrapper">
|
<div class="search-bar">
|
%s
|
</div>
|
</div>
|
|
<!-- Create new term section -->
|
<details class="create-term" hidden>
|
<summary class="row btw">Add New Term</summary>
|
<div class="create-new-term-section">
|
<form class="create-term" data-nocache data-form-id="create-term" data-save="terms">
|
<div class="form-group">
|
<label for="term_name">Term Name:</label>
|
<input type="text" name="term_name" id="term_name" required>
|
</div>
|
|
<div class="form-group">
|
<label for="select_parent">Nest it under:</label>
|
<select name="parent" id="select_parent">
|
<option value="0">None (Top Level)</option>
|
</select>
|
</div>
|
</form>
|
|
</div>
|
</details>
|
%s
|
</div>
|
</dialog>
|
<template class="loadingItems">
|
<p>{ <span>loading items</span> }</p>
|
</template>
|
<template class="autocompleteItem">
|
<li class="autocomplete item btn"></li>
|
</template>
|
<template class="noTermResults">
|
<p>{ <span>nothing found</span> }</p>
|
</template>
|
<template class="termListItem">
|
<li>
|
<input type="checkbox">
|
<label>
|
<span class="term-name"></span>
|
</label>
|
</li>
|
</template>
|
<template class="termChildrenToggle">
|
<button type="button" class="toggle-children" aria-expanded="false">
|
%s
|
</button>
|
</template>
|
<template class="selectedTerm">
|
<div class="selected-item row">
|
<span class="item-name"></span>
|
<button type="button" class="remove-term row">%s</button>
|
</div>
|
</template>
|
<template class="termBreadcrumb">
|
<button type="button" class="path-level"></button>
|
</template>',
|
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(
|
'<div class="search-container row start nowrap">
|
<input type="search" id="%s" placeholder="%s">
|
<button title="Clear Search" type="button" class="clear-search" aria-label="Clear search"
|
onclick="this.previousElementSibling.value = \'\'; this.previousElementSibling.focus();">%s</button>
|
<button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">%s</button>
|
</div>',
|
$id,
|
$placeholder,
|
jvbIcon('x', ['title' => 'Clear Search']),
|
jvbIcon('magnifying-glass')
|
);
|
}
|
}
|