<?php
|
namespace JVBase\inc\meta;
|
|
use JVBase\forms\TaxonomySelector;
|
use JVBase\forms\PostSelector;
|
use DateTime;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
/**
|
* Renders the form fields for managing the meta
|
*/
|
class MetaFormOld
|
{
|
protected int $max_file_size = 5242880;
|
|
//Rendering fields
|
public function render(string $name, mixed $value, array $config, bool $showHidden = false, bool $return = false):mixed
|
{
|
$out = '';
|
if (jvbCheck('hidden', $config) && !$showHidden) {
|
return $out;
|
}
|
// Get conditional attributes if they exist
|
$conditional = array_key_exists('condition', $config) ?
|
$this->handleConditionalField($config) : '';
|
|
if (!array_key_exists('type', $config)) {
|
return $out;
|
}
|
|
if (array_key_exists('display', $config) && $config['display'] === 'hidden'){
|
$out = '<input type="hidden" name="'.$name.'" value="'.$value.'" />';
|
if (!$return) {
|
echo $out;
|
}
|
return $out;
|
}
|
|
ob_start();
|
$type = array_map( 'ucfirst', explode('_', $config['type']));
|
$type = implode('', $type);
|
$method = 'render' . $type . 'Field';
|
|
$nameTemp = implode('', array_map('ucfirst', explode('_', $name)));
|
$nameMethod = 'render'.$nameTemp.'Field';
|
if(function_exists($nameMethod)) {
|
call_user_func($nameMethod, $value, $config);
|
} elseif (function_exists($method)) {
|
call_user_func($method, $value, $config);
|
} elseif (method_exists($this, $method)) {
|
$this->$method($name, $value, $config);
|
}
|
|
|
|
$out = ob_get_clean();
|
|
do_action('jvbRenderFormField', $name, $config, $value);
|
$out = apply_filters('jvbFilterRenderFormField', $out, $name, $config, $value);
|
if (!$return) {
|
echo $out;
|
}
|
return $out;
|
}
|
|
public function renderTextField(string $name, mixed $value, array $field):void
|
{
|
// Use field-specific value if provided, otherwise use the meta value
|
$display_value = isset($field['value']) ? $field['value'] : $value;
|
$conditional = $this->handleConditionalField($field);
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
|
$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
|
?>
|
<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
|
<label for="<?= esc_attr($name); ?>">
|
<?= esc_html($field['label']); ?>
|
<?php if (!empty($field['limit'])) : ?>
|
<span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
|
<span class="current">0</span>/<?= esc_attr($field['limit']); ?>
|
</span>
|
<?php endif; ?>
|
</label>
|
<input
|
type="<?= esc_attr($field['subtype']??'text'); ?>"
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($display_value); ?>"
|
<?= $placeholder ?>
|
<?= $autocomplete ?>
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
<?= $describedBy ?>
|
>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
if (array_key_exists('limit', $field)) {
|
$this->outputCharacterCountJS();
|
}
|
}
|
|
private function renderTelField(string $name, mixed $value, array $field):void
|
{$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
// Use field-specific value if provided, otherwise use the meta value
|
$display_value = isset($field['value']) ? $field['value'] : $value;
|
$conditional = $this->handleConditionalField($field);
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
|
$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
|
?>
|
<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
|
<label for="<?= esc_attr($name); ?>">
|
<?= esc_html($field['label']); ?>
|
<?php if (!empty($field['limit'])) : ?>
|
<span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
|
<span class="current">0</span>/<?= esc_attr($field['limit']); ?>
|
</span>
|
<?php endif; ?>
|
</label>
|
<input
|
type="tel"
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($display_value); ?>"
|
<?= $placeholder ?>
|
<?= $autocomplete?>
|
<?= $describedBy ?>
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
if (array_key_exists('limit', $field)) {
|
$this->outputCharacterCountJS();
|
}
|
}
|
private function renderEmailField(string $name, mixed $value, array $field):void
|
{
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
// Use field-specific value if provided, otherwise use the meta value
|
$display_value = isset($field['value']) ? $field['value'] : $value;
|
$conditional = $this->handleConditionalField($field);
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
|
$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
|
?>
|
<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
|
<label for="<?= esc_attr($name); ?>">
|
<?= esc_html($field['label']); ?>
|
<?php if (!empty($field['limit'])) : ?>
|
<span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
|
<span class="current">0</span>/<?= esc_attr($field['limit']); ?>
|
</span>
|
<?php endif; ?>
|
</label>
|
<input
|
type="email"
|
<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($display_value); ?>"
|
<?= $placeholder ?>
|
<?= $autocomplete ?>
|
<?= $describedBy ?>
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
if (array_key_exists('limit', $field)) {
|
$this->outputCharacterCountJS();
|
}
|
}
|
|
private function renderUrlField(string $name, mixed $value, array $field):void
|
{
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
// Use field-specific value if provided, otherwise use the meta value
|
$display_value = isset($field['value']) ? $field['value'] : $value;
|
$conditional = $this->handleConditionalField($field);
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
|
$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
|
?>
|
<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
|
<label for="<?= esc_attr($name); ?>">
|
<?= esc_html($field['label']); ?>
|
<?php if (!empty($field['limit'])) : ?>
|
<span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
|
<span class="current">0</span>/<?= esc_attr($field['limit']); ?>
|
</span>
|
<?php endif; ?>
|
</label>
|
<input
|
type="url"
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($display_value); ?>"
|
<?= $placeholder ?>
|
<?= $describedBy ?>
|
<?= $autocomplete ?>
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
if (array_key_exists('limit', $field)) {
|
$this->outputCharacterCountJS();
|
}
|
}
|
|
private function renderNumberField(string $name, mixed $value, array $field):void
|
{
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
$description = '<ul class="list-none"><li>Tip: hold Ctrl/Command to increase 5x</li><li>Shift to increase 10x,</li><li>Or Ctrl/Command + Shift to increase 50x</li></ul>';
|
$description .= $field['description']??'';
|
$conditional = $this->handleConditionalField($field);
|
|
$min = isset($field['min']) ? (float)$field['min'] : 0;
|
$max = isset($field['max']) ? (float)$field['max'] : 100;
|
$step = isset($field['step']) ? (float)$field['step'] : 1;
|
|
$data = '';
|
if (array_key_exists('data', $field) && !empty($field['data'])) {
|
foreach($field['data'] as $key => $v) {
|
if ($v === '') {
|
$data .= ' data-'.$key;
|
} else {
|
$data .= ' data-'.$key.'="'.$v.'"';
|
}
|
}
|
}
|
|
if (empty($value)) {
|
$value = $field['default']??0;
|
}
|
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
|
?>
|
<div class="field <?=$field['type']?> <?=$name?> row" <?=$conditional?> data-field="<?=$name?>">
|
<label for="<?= esc_attr($name); ?>">
|
<?= esc_html($field['label']); ?>
|
</label>
|
|
<div class="quantity"
|
<?=$data?>>
|
|
<button type="button"
|
class="decrease"
|
title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>"
|
aria-label="Decrease <?= esc_attr($field['label']); ?>">
|
<?= jvbIcon('minus')?>
|
</button>
|
|
<input type="number"
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($value); ?>"
|
min="<?= esc_attr($min); ?>"
|
max="<?= esc_attr($max); ?>"
|
step="<?= esc_attr($step); ?>"
|
class="quantity-input"
|
<?= $describedBy ?>
|
<?= $autocomplete ?>
|
<?= !empty($field['required']) ? 'required' : ''; ?>>
|
|
<button type="button"
|
class="increase"
|
title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>"
|
aria-label="Increase <?= esc_attr($field['label']); ?>">
|
<?= jvbIcon('add')?>
|
</button>
|
</div>
|
<?php $this->renderDescription($description, $name); ?>
|
</div>
|
<?php
|
}
|
|
public function renderTextareaField(string $name, mixed $value, array $field):void
|
{
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
$rows = isset($field['rows']) ? (int)$field['rows'] : 4;
|
$conditional = $this->handleConditionalField($field);
|
$quill = (array_key_exists('quill', $field) && $field['quill'] == true) ? ' data-editor="true"' : '';
|
if ($quill !== '') {
|
$allowImages = array_key_exists('allowImage', $field);
|
$quill .= ($allowImages) ? ' data-allowimage="true"' : ' data-allowimage="false"';
|
}
|
// Handle array values
|
if (is_array($value)) {
|
$value = implode(', ', $value);
|
}
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
|
?>
|
<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
|
<label for="<?= esc_attr($name ?? ''); ?>">
|
<?= esc_html($field['label']); ?>
|
<?php if (!empty($field['limit'])) : ?>
|
<span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
|
<span class="current">0</span>/<?= esc_attr($field['limit']); ?>
|
</span>
|
<?php endif; ?>
|
</label>
|
<textarea
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name ?? ''); ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name ?? ''); ?>"
|
<?= $quill ?>
|
rows="<?= esc_attr($rows); ?>"
|
<?= $placeholder ?>
|
<?= $describedBy ?>
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
<?= !empty($field['limit']) ? 'data-limit="' . esc_attr($field['limit']) . '"' : ''; ?>
|
><?= esc_textarea($value); ?></textarea>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
}
|
|
private function renderSetField(string $name, mixed $value, array $field):void
|
{
|
$this->renderCheckboxField($name, $value, $field);
|
}
|
|
private function renderCheckboxField(string $name, mixed $value, array $field):void
|
{
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
$value = !is_array($value) ? explode(',', $value) : $value;
|
$limit = isset($field['limit']) ? (int)$field['limit'] : 0;
|
$conditional = $this->handleConditionalField($field);
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
?>
|
<div class="field checkbox-group" <?= $limit ? 'data-limit="' . esc_attr($limit) . '"' : ''; ?> <?=$conditional?> data-field="<?=$name?>"<?=$describedBy?>>
|
<span class="label"><?= esc_html($field['label']); ?></span>
|
<div class="checkbox-options flex">
|
<?php foreach ($field['options'] as $key => $label) : ?>
|
<input
|
type="checkbox"
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name).'-'.esc_attr($key)?>"
|
<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name)?>[]"
|
value="<?= esc_attr($key); ?>"
|
<?= (in_array($key, $value)) ? 'checked' : ''; ?>
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
>
|
<label class="checkbox-option" for="<?= esc_attr($name).'-'.esc_attr($key) ?>">
|
<?= esc_html($label); ?>
|
</label>
|
<?php endforeach; ?>
|
</div>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
}
|
private function renderRadioField(string $name, mixed $value, array $field):void
|
{
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
$value = (array)$value;
|
$conditional = $this->handleConditionalField($field);
|
|
if (!array_key_exists('label', $field)) {
|
error_log('No label for: '.print_r($name, true));
|
}
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
?>
|
<div class="field radio-group"<?=$conditional?> data-field="<?=$name?>"<?=$describedBy?>>
|
<label><?= esc_html($field['label']); ?></label>
|
|
<div class="radio-options row">
|
<?php foreach ($field['options'] as $key => $label) : ?>
|
<input
|
type="radio"
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name).'-'.esc_attr($key) ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($key); ?>"
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
<?= (in_array($key, $value)) ? 'checked' : ''; ?>
|
>
|
<label class="radio-option" for="<?= esc_attr($name).'-'.esc_attr($key) ?>">
|
<?= esc_html($label); ?>
|
</label>
|
<?php endforeach; ?>
|
</div>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
|
</div>
|
<?php
|
}
|
|
private function renderRepeaterField(string $name, mixed $value, array $field):void
|
{
|
error_log('Rendering Repeater Field!');
|
$values = is_array($value) ? $value : array();
|
|
$conditional = $this->handleConditionalField($field);
|
$row_label = isset($field['row_label']) ? $field['row_label'] : '';
|
$rowTitle = (array_key_exists('new_row', $field)) ? $field['new_row'] : 'New Item';
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
?>
|
<div class="field repeater <?=$name?>"
|
data-field="<?= esc_attr($name); ?>"
|
<?= $describedBy ?>
|
<?= $row_label ? 'data-label="' . esc_attr($row_label) . '"' : ''; ?>
|
<?=$conditional?>>
|
<?php
|
if (!array_key_exists('label', $field)) {
|
error_log('No label for: '.print_r($name, true));
|
}
|
?>
|
<h3><?= esc_html($field['label']); ?></h3>
|
|
|
<div class="repeater-items">
|
<?php
|
if (!empty($values)) {
|
foreach ($values as $index => $row) {
|
$this->renderRepeaterRow($field['fields'], $row, $index, $name, $rowTitle);
|
}
|
}
|
?>
|
</div>
|
|
<template class="<?=uniqid('repeaterTemplate')?>">
|
<?php $this->renderRepeaterRow($field['fields'], array(), '', '', $rowTitle); ?>
|
</template>
|
|
<button type="button" class="add-repeater-row">
|
<?= jvbIcon('plus', ['title'=> 'Add']); ?> <?= (array_key_exists('add_label', $field)) ? $field['add_label'] : 'Add Item'; ?>
|
</button>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
}
|
|
private function renderRepeaterRow(array $fields, array $values, int|string $index, string $base_name, string $rowTitle = 'New Item'):void
|
{
|
$display_number = (is_string($index)) ? $index : ($index + 1);
|
?>
|
<div class="repeater-row" data-index="<?= esc_attr($index); ?>">
|
<details <?= (is_string($index)) ? 'open' : ''; ?>>
|
<summary class="repeater-row-header row btw">
|
<span class="drag-handle"><?= jvbIcon('grab'); ?></span>
|
<span class="row-number">#<?= esc_html($display_number); ?></span>
|
<span class="row-title"><?= esc_html($this->getRowTitle($fields, $values, $rowTitle)); ?></span>
|
<button type="button" class="remove-row" title="Remove">
|
<?= jvbIcon('delete', ['title'=>'Remove']); ?>
|
</button>
|
</summary>
|
<div class="repeater-row-content">
|
<?php
|
foreach ($fields as $slug => $field) :
|
if ($base_name === '') {
|
$field_name = $slug;
|
} else {
|
$field_name = sprintf('%s:%s:%s', $base_name, $index, $slug);
|
}
|
$field_value = isset($values[$slug]) ? $values[$slug] : '';
|
$name = $field_name;
|
$this->render($name, $field_value, $field);
|
endforeach;
|
?>
|
</div>
|
</details>
|
</div>
|
<?php
|
}
|
|
private function getRowTitle(array $fields, array $values, string $rowTitle):string
|
{
|
// Try to find the first text field or textarea value to use as title
|
foreach ($fields as $slug => $field) {
|
if (in_array($field['type'], ['text', 'textarea']) &&
|
isset($values[$slug]) &&
|
!empty($values[$slug])) {
|
return $values[$slug];
|
}
|
}
|
return $rowTitle;
|
}
|
|
private function renderTaxonomyField(string $name, mixed $value, array $field):void
|
{
|
$conditional = $this->handleConditionalField($field);
|
$taxonomy = $field['taxonomy'];
|
|
// Get currently selected terms
|
$selected_terms = ($value === '') ? [] : explode(',', $value);
|
|
|
// Convert selected term IDs to the format expected by single modal
|
$processedSelected = [];
|
if (!empty($selected_terms)) {
|
foreach ($selected_terms as $termId) {
|
if (is_numeric($termId)) {
|
$term = get_term($termId, $taxonomy);
|
if ($term && !is_wp_error($term)) {
|
$processedSelected[$term->term_id] = [
|
'name' => $term->name,
|
'path' => TaxonomySelector::getTermPath($term)
|
];
|
}
|
}
|
}
|
}
|
|
// Create configuration for single modal system
|
$config = [
|
'taxonomy' => $taxonomy,
|
'max' => $field['limit'] ?? 0,
|
'search' => $field['search'] ?? true,
|
'createNew' => $field['createNew'] ?? false,
|
'selected' => $processedSelected,
|
'base' => $field['base'] ?? '',
|
];
|
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
?>
|
<div class="field taxonomy <?=$name?>" <?= $conditional ?> data-field="<?=$name?>">
|
<div class="field-group-header">
|
<label class="toggle">
|
<?= jvbIcon(str_replace(BASE, '', $taxonomy)) ?>
|
<?= esc_html($field['label']) ?>
|
</label>
|
</div>
|
|
<?php
|
$tax = new TaxonomySelector($name, $taxonomy, $config);
|
$extra = '<input type="hidden"
|
name="'.esc_attr($name).'"
|
id="'.esc_attr($name).'"'.$describedBy.'
|
data-taxonomy="'.esc_attr($taxonomy).'"
|
value="'.esc_attr(is_array($selected_terms) ? implode(',', $selected_terms) : $selected_terms).'">';
|
echo $tax->render([], $extra);
|
?>
|
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
}
|
|
protected function renderPostSelectorField(string $name, mixed $value, array $field):void
|
{
|
$conditional = $this->handleConditionalField($field);
|
|
// Process selected posts
|
$selected_posts = $value;
|
if (is_string($selected_posts)) {
|
$selected_posts = !empty($selected_posts) ? explode(',', $selected_posts) : [];
|
} elseif (!is_array($selected_posts)) {
|
$selected_posts = [];
|
}
|
|
// Configure the post selector
|
$config = [
|
'multiple' => $field['multiple'] ?? true,
|
'maxSelections' => $field['limit'] ?? 0,
|
'search' => true,
|
'placeholder' => $field['placeholder'] ?? 'Search posts...',
|
'noResults' => 'No posts found',
|
'shop_id' => $field['shop_id'] ?? null,
|
'onClose' => 'updateMetaFormPost'
|
];
|
|
$postSelector = new PostSelector($field['post_type'], $config);
|
$containerId = $name . '-post-selector';
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
?>
|
<div class="field post-selector <?=$name?>" <?= $conditional ?> data-field="<?=$name?>">
|
<div class="field-group-header">
|
<label class="toggle">
|
<?= jvbIcon($field['post_type'] . '-selector') ?>
|
<?= esc_html($field['label'] ?? ucfirst($field['post_type'])) ?>
|
</label>
|
<button title="Add <?= esc_attr(ucfirst($field['post_type'])) ?>"
|
class="add-item-btn"
|
type="button">
|
<?= jvbIcon('add', ['title' => "Add " . ucfirst($field['post_type'])]) ?>
|
</button>
|
</div>
|
|
<?= $postSelector->render($selected_posts, $containerId) ?>
|
|
<!-- Hidden input for form submission -->
|
<input type="hidden"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name) ?>"
|
class="post-selector-input"
|
<?= $describedBy ?>
|
data-post-type="<?= esc_attr($field['post_type']) ?>"
|
value="<?= esc_attr(is_array($selected_posts) ? implode(',', $selected_posts) : $selected_posts) ?>">
|
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
}
|
|
protected function renderGroupField(string $name, mixed $value, array $field):void
|
{
|
if (!array_key_exists('fields', $field) || empty($field['fields'])) {
|
return;
|
}
|
|
// Handle conditional fields
|
$conditional = $this->handleConditionalField($field);
|
|
// Ensure value is an array
|
$values = is_array($value) ? $value : [];
|
$original = $name;
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
$hidden = (array_key_exists('mode', $field) && $field['mode'] === 'hidden');
|
if (!$hidden) {
|
?>
|
<fieldset class="field group <?= esc_attr($name) ?>" <?= $conditional ?> data-field="<?=$name?>"<?= $describedBy?>>
|
<legend><?= esc_html($field['label']) ?></legend>
|
<?php
|
}
|
?>
|
|
<div class="group-fields <?=$original?>"<?= ($hidden) ? ' data-field="'.$name.'"' : ''?>"<?= $describedBy ?>>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
<?php
|
foreach ($field['fields'] as $field_name => $config) {
|
// Set the group context for proper field naming
|
$config['group'] = $name;
|
|
// Get the value for this specific field
|
$field_value = $values[$field_name] ?? '';
|
|
// Handle conditional fields within the group
|
if (isset($config['condition'])) {
|
// Convert condition field reference to group context
|
$condition_field = $config['condition']['field'];
|
if (!str_contains($condition_field, '::')) {
|
$config['condition']['field'] = $name . '::' . $condition_field;
|
}
|
}
|
|
$this->render($field_name, $field_value, $config);
|
}
|
?>
|
</div>
|
<?php
|
if (!$hidden) {
|
?>
|
</fieldset>
|
<?php
|
}
|
}
|
protected function renderLocationField(string $name, mixed $value, array $field): void
|
{
|
$googleMaps = JVB()->connect('maps');
|
if (!$googleMaps->isSetUp()) {
|
echo '<div class="notice notice-warning"><p>Google Maps not configured. Please configure in Integrations settings.</p></div>';
|
return;
|
}
|
|
// Extract stored values
|
if (is_string($value)) {
|
$value = maybe_unserialize($value);
|
}
|
$stored_data = is_array($value) ? $value : [];
|
|
$address = $stored_data['address'] ?? '';
|
$lat = $stored_data['lat'] ?? '';
|
$lng = $stored_data['lng'] ?? '';
|
|
// Generate unique field ID
|
$field_id = esc_attr($name);
|
$map_id = $field_id . '_map';
|
|
// Handle grouped fields
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'] . '::' . $name;
|
}
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
|
// Prepare configuration for JavaScript initialization
|
$js_config = [
|
'fieldId' => $field_id,
|
'initialCoords' => (!empty($lat) && !empty($lng)) ? [
|
'lat' => (float)$lat,
|
'lng' => (float)$lng
|
] : null
|
];
|
|
// IMPORTANT: Properly escape the JSON for use in HTML attribute
|
$json_config = htmlspecialchars(json_encode($js_config), ENT_QUOTES, 'UTF-8');
|
?>
|
|
<div class="field location <?= esc_attr($field_id) ?>"
|
data-field="<?= esc_attr($field_id) ?>"
|
data-location-field-init="<?= $json_config ?>"<?=$describedBy?>>
|
|
<?php
|
if (!empty($stored_data['street'])) {
|
echo '<p><b>Current location:</b> '.esc_html($stored_data['street']).'</p>';
|
echo '<p class="hint"><b>Search below to change:</b></p>';
|
}
|
?>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
|
<div class="location-search-wrapper">
|
<div class="autocomplete-wrapper"></div>
|
|
<!-- Map container -->
|
<div class="location-preview">
|
<div id="<?= esc_attr($map_id); ?>"
|
class="location-map">
|
</div>
|
|
<?php if (!empty($stored_data)):
|
jvbLocationLinks($stored_data);
|
endif; ?>
|
</div>
|
|
<!-- Hidden inputs for data storage -->
|
<input type="hidden"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[address]"
|
value="<?= esc_attr($address); ?>"
|
data-location-field="address">
|
|
<input type="hidden"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[lat]"
|
value="<?= esc_attr($lat); ?>"
|
data-location-field="lat">
|
|
<input type="hidden"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[lng]"
|
value="<?= esc_attr($lng); ?>"
|
data-location-field="lng">
|
|
<?php
|
// Component fields
|
$components = ['street', 'city', 'province', 'postal_code', 'country'];
|
foreach ($components as $component):
|
?>
|
<input type="hidden"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[<?= $component; ?>]"
|
value="<?= esc_attr($stored_data[$component] ?? ''); ?>"
|
data-location-field="<?= esc_attr($component); ?>">
|
<?php endforeach; ?>
|
|
</div>
|
</div>
|
<?php
|
}
|
//TODO: This is more or less handled by PostSelector/TaxonomySelector, no?
|
private function renderAssociationField(string $name, mixed $value, array $field):void
|
{
|
// Ensure value is an array
|
if (!is_array($value)) {
|
$value = empty($value) ? [] : [$value];
|
}
|
|
// Get field configuration
|
$limit = isset($field['limit']) ? (int)$field['limit'] : 0;
|
$object_types = isset($field['object_types']) ? $field['object_types'] : ['post'];
|
$post_types = isset($field['post_types']) ? $field['post_types'] : ['post'];
|
$taxonomies = isset($field['taxonomies']) ? $field['taxonomies'] : [];
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
// Create unique ID for this field
|
$field_id = 'association-' . esc_attr($name);
|
$conditional = $this->handleConditionalField($field);
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
?>
|
<div class="field association <?=$name?>" data-field="<?= esc_attr($name); ?>" <?= $conditional; ?>>
|
<label><?= esc_html($field['label']); ?></label>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
|
<div class="association-container"<?=$describedBy?>>
|
<div class="association-search">
|
<input type="text"
|
id="<?= esc_attr($field_id); ?>-search"
|
class="association-search-input"
|
placeholder="Search items...">
|
|
<div class="association-filter">
|
<?php if (count($object_types) > 1 || count($post_types) > 1 || count($taxonomies) > 0) : ?>
|
<select class="association-filter-select">
|
<?php if (in_array('post', $object_types)) : ?>
|
<?php foreach ($post_types as $post_type) : ?>
|
<?php
|
$post_type_obj = get_post_type_object($post_type);
|
$label = $post_type_obj ? $post_type_obj->labels->singular_name : ucfirst($post_type);
|
?>
|
<option value="post:<?= esc_attr($post_type); ?>">
|
<?= esc_html($label); ?>
|
</option>
|
<?php endforeach; ?>
|
<?php endif; ?>
|
|
<?php if (in_array('term', $object_types)) : ?>
|
<?php foreach ($taxonomies as $taxonomy) : ?>
|
<?php
|
$tax_obj = get_taxonomy($taxonomy);
|
$label = $tax_obj ? $tax_obj->labels->singular_name : ucfirst($taxonomy);
|
?>
|
<option value="term:<?= esc_attr($taxonomy); ?>">
|
<?= esc_html($label); ?>
|
</option>
|
<?php endforeach; ?>
|
<?php endif; ?>
|
</select>
|
<?php endif; ?>
|
|
<button type="button" class="search-button">
|
<?= jvbIcon('search', ['title' => 'Search']); ?>
|
</button>
|
</div>
|
</div>
|
|
<div class="association-results">
|
<div class="association-available">
|
<h4>Available Items</h4>
|
<ul class="available-items"></ul>
|
<div class="association-loading" hidden>
|
Loading...
|
</div>
|
<div class="association-no-results" hidden>
|
No items found
|
</div>
|
<div class="association-pagination">
|
<button type="button" class="prev-page" disabled>
|
<?= jvbIcon('arrow-left', ['title' => 'Previous']); ?>
|
</button>
|
<span class="page-info">Page <span class="current-page">1</span></span>
|
<button type="button" class="next-page" disabled>
|
<?= jvbIcon('arrow-right', ['title' => 'Next']); ?>
|
</button>
|
</div>
|
</div>
|
|
<div class="association-actions">
|
<button type="button" class="add-selected" disabled>
|
<?= jvbIcon('arrow-right', ['title' => 'Add selected']); ?>
|
</button>
|
<button type="button" class="remove-selected" disabled>
|
<?= jvbIcon('arrow-left', ['title' => 'Remove selected']); ?>
|
</button>
|
</div>
|
|
<div class="association-selected">
|
<h4>Selected Items
|
<?php if ($limit) : ?>
|
<span class="limit-info">(<?= esc_html($limit); ?> max)</span>
|
<?php endif; ?>
|
</h4>
|
<ul class="selected-items row">
|
<?php
|
// Display currently selected items
|
foreach ($value as $item_id) {
|
// Try to determine the type and get details
|
$item_type = '';
|
$item_title = '';
|
$item_object = '';
|
|
// Check if it's a post
|
if (in_array('post', $object_types)) {
|
$post = get_post($item_id);
|
if ($post && in_array($post->post_type, $post_types)) {
|
$item_type = 'post';
|
$item_title = $post->post_title;
|
$item_object = $post->post_type;
|
}
|
}
|
|
// Check if it's a term
|
if (empty($item_type) && in_array('term', $object_types)) {
|
foreach ($taxonomies as $taxonomy) {
|
$term = get_term($item_id, $taxonomy);
|
if (!is_wp_error($term) && $term) {
|
$item_type = 'term';
|
$item_title = $term->name;
|
$item_object = $term->taxonomy;
|
break;
|
}
|
}
|
}
|
|
// Only output if we found the item
|
if (!empty($item_type) && !empty($item_title)) {
|
?>
|
<li data-id="<?= esc_attr($item_id); ?>"
|
data-type="<?= esc_attr($item_type); ?>"
|
data-object="<?= esc_attr($item_object); ?>">
|
<span class="item-title"><?= esc_html($item_title); ?></span>
|
<span class="item-type"><?= esc_html(ucfirst($item_object)); ?></span>
|
<button type="button" class="remove-item row">
|
<?= jvbIcon('close', ['title' => 'Remove']); ?>
|
</button>
|
</li>
|
<?php
|
}
|
}
|
?>
|
</ul>
|
</div>
|
</div>
|
</div>
|
|
<!-- Hidden input to store selected values -->
|
<input type="hidden" name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>" value="<?= esc_attr(implode(',', $value)); ?>">
|
</div>
|
|
<script>
|
(function() {
|
// Initialize association field
|
const container = document.querySelector('[data-field="<?= esc_attr($name); ?>"]');
|
if (!container) return;
|
|
const searchInput = container.querySelector('.association-search-input');
|
const filterSelect = container.querySelector('.association-filter-select');
|
const searchButton = container.querySelector('.search-button');
|
const availableList = container.querySelector('.available-items');
|
const selectedList = container.querySelector('.selected-items');
|
const hiddenInput = container.querySelector('input[name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"]');
|
const addButton = container.querySelector('.add-selected');
|
const removeButton = container.querySelector('.remove-selected');
|
const loadingIndicator = container.querySelector('.association-loading');
|
const noResultsMessage = container.querySelector('.association-no-results');
|
const prevPageButton = container.querySelector('.prev-page');
|
const nextPageButton = container.querySelector('.next-page');
|
const currentPageSpan = container.querySelector('.current-page');
|
|
// Configuration
|
const config = {
|
limit: <?= $limit ?: 0; ?>,
|
objectTypes: <?= json_encode($object_types); ?>,
|
postTypes: <?= json_encode($post_types); ?>,
|
taxonomies: <?= json_encode($taxonomies); ?>,
|
perPage: 10,
|
currentPage: 1
|
};
|
|
// Current state
|
let currentSearch = '';
|
let currentFilter = filterSelect ? filterSelect.value : (config.objectTypes.includes('post') ? 'post:' + config.postTypes[0] : 'term:' + config.taxonomies[0]);
|
let availableItems = [];
|
let selectedItems = [];
|
|
// Parse initial selected items
|
const initialValue = hiddenInput.value;
|
if (initialValue) {
|
selectedItems = initialValue.split(',').map(id => parseInt(id, 10));
|
}
|
|
// Event Listeners
|
if (searchButton) {
|
searchButton.addEventListener('click', performSearch);
|
}
|
|
if (searchInput) {
|
searchInput.addEventListener('keypress', function(e) {
|
if (e.key === 'Enter') {
|
e.preventDefault();
|
performSearch();
|
}
|
});
|
}
|
|
if (filterSelect) {
|
filterSelect.addEventListener('change', function() {
|
currentFilter = this.value;
|
config.currentPage = 1;
|
performSearch();
|
});
|
}
|
|
if (prevPageButton) {
|
prevPageButton.addEventListener('click', function() {
|
if (config.currentPage > 1) {
|
config.currentPage--;
|
performSearch();
|
}
|
});
|
}
|
|
if (nextPageButton) {
|
nextPageButton.addEventListener('click', function() {
|
config.currentPage++;
|
performSearch();
|
});
|
}
|
|
// Add items
|
addButton.addEventListener('click', function() {
|
const selectedAvailableItems = availableList.querySelectorAll('li.selected');
|
selectedAvailableItems.forEach(item => {
|
const id = parseInt(item.dataset.id, 10);
|
// Check limit
|
if (config.limit && selectedItems.length >= config.limit) {
|
return;
|
}
|
|
// Skip if already selected
|
if (selectedItems.includes(id)) {
|
return;
|
}
|
|
// Add to selection
|
selectedItems.push(id);
|
|
// Clone and modify for selected list
|
const clone = item.cloneNode(true);
|
clone.classList.remove('selected');
|
|
// Replace checkbox with remove button
|
const checkbox = clone.querySelector('input[type="checkbox"]');
|
if (checkbox) {
|
const removeBtn = document.createElement('button');
|
removeBtn.type = 'button';
|
removeBtn.className = 'remove-item';
|
removeBtn.innerHTML = '<?= jvbIcon('close', ['title' => 'Remove']); ?>';
|
removeBtn.addEventListener('click', function() {
|
removeItem(id, clone);
|
});
|
|
checkbox.parentNode.replaceChild(removeBtn, checkbox);
|
}
|
|
selectedList.appendChild(clone);
|
});
|
|
// Update hidden input
|
updateHiddenInput();
|
|
// Update UI state
|
updateButtonStates();
|
});
|
|
// Remove items
|
removeButton.addEventListener('click', function() {
|
const selectedSelectedItems = selectedList.querySelectorAll('li.selected');
|
selectedSelectedItems.forEach(item => {
|
const id = parseInt(item.dataset.id, 10);
|
removeItem(id, item);
|
});
|
});
|
|
// Listen for clicks on items in both lists
|
availableList.addEventListener('click', function(e) {
|
const item = e.target.closest('li');
|
if (!item) return;
|
|
// If clicking checkbox, handle separately
|
if (e.target.type === 'checkbox') {
|
updateButtonStates();
|
return;
|
}
|
|
// Toggle selection
|
if (item.classList.contains('selected')) {
|
item.classList.remove('selected');
|
item.querySelector('input[type="checkbox"]').checked = false;
|
} else {
|
item.classList.add('selected');
|
item.querySelector('input[type="checkbox"]').checked = true;
|
}
|
|
updateButtonStates();
|
});
|
|
selectedList.addEventListener('click', function(e) {
|
const item = e.target.closest('li');
|
if (!item) return;
|
|
// If clicking remove button, handle it
|
if (e.target.closest('.remove-item')) {
|
const id = parseInt(item.dataset.id, 10);
|
removeItem(id, item);
|
return;
|
}
|
|
// Toggle selection
|
item.classList.toggle('selected');
|
updateButtonStates();
|
});
|
|
// Helper Functions
|
function performSearch() {
|
currentSearch = searchInput.value.trim();
|
|
// Show loading
|
loadingIndicator.hidden = false;
|
noResultsMessage.hidden = true;
|
availableList.innerHTML = '';
|
|
// Get filter parts
|
const [type, object] = currentFilter.split(':');
|
|
// Prepare data for AJAX
|
const data = {
|
action: 'jvb_association_search',
|
nonce: jvbSettings.nonce,
|
type: type,
|
object: object,
|
search: currentSearch,
|
page: config.currentPage,
|
per_page: config.perPage,
|
selected: selectedItems
|
};
|
|
// Make AJAX request to WordPress REST API
|
fetch(jvbSettings.api + 'terms', {
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': jvbSettings.nonce
|
},
|
body: JSON.stringify(data)
|
})
|
.then(response => response.json())
|
.then(response => {
|
// Hide loading
|
loadingIndicator.hidden = true;
|
|
if (response.success && response.items && response.items.length > 0) {
|
// Update available items
|
availableItems = response.items;
|
|
// Render items
|
renderAvailableItems();
|
|
// Update pagination
|
updatePagination(response.total, response.pages);
|
} else {
|
// Show no results
|
noResultsMessage.hidden = false;
|
prevPageButton.disabled = true;
|
nextPageButton.disabled = true;
|
currentPageSpan.textContent = '1';
|
}
|
})
|
.catch(error => {
|
console.error('Error searching items:', error);
|
loadingIndicator.hidden = true;
|
noResultsMessage.hidden = false;
|
});
|
}
|
|
function renderAvailableItems() {
|
availableList.innerHTML = '';
|
|
availableItems.forEach(item => {
|
const isSelected = selectedItems.includes(item.id);
|
|
const li = document.createElement('li');
|
li.dataset.id = item.id;
|
li.dataset.type = item.type;
|
li.dataset.object = item.object;
|
|
// Create checkbox
|
const checkbox = document.createElement('input');
|
checkbox.type = 'checkbox';
|
checkbox.id = `${name}-item-${item.id}`;
|
|
// Create label for title
|
const titleSpan = document.createElement('span');
|
titleSpan.className = 'item-title';
|
titleSpan.textContent = item.title;
|
|
// Create label for type
|
const typeSpan = document.createElement('span');
|
typeSpan.className = 'item-type';
|
typeSpan.textContent = item.object_label;
|
|
// Append elements
|
li.appendChild(checkbox);
|
li.appendChild(titleSpan);
|
li.appendChild(typeSpan);
|
|
// Disable if already selected
|
if (isSelected) {
|
li.classList.add('disabled');
|
checkbox.disabled = true;
|
|
// Add note that item is already selected
|
const note = document.createElement('span');
|
note.className = 'item-note';
|
note.textContent = 'Already selected';
|
li.appendChild(note);
|
}
|
|
availableList.appendChild(li);
|
});
|
}
|
|
function updatePagination(total, pages) {
|
// Update current page display
|
currentPageSpan.textContent = config.currentPage;
|
|
// Update prev/next buttons
|
prevPageButton.disabled = config.currentPage <= 1;
|
nextPageButton.disabled = config.currentPage >= pages;
|
}
|
|
function removeItem(id, element) {
|
// Remove from array
|
selectedItems = selectedItems.filter(itemId => itemId !== id);
|
|
// Remove from DOM
|
if (element) {
|
element.remove();
|
}
|
|
// Update hidden input
|
updateHiddenInput();
|
|
// Update buttons
|
updateButtonStates();
|
}
|
|
function updateHiddenInput() {
|
hiddenInput.value = selectedItems.join(',');
|
}
|
|
function updateButtonStates() {
|
// Add button is enabled if at least one available item is selected
|
// and we haven't reached the limit
|
const hasSelectedAvailable = availableList.querySelector('li.selected:not(.disabled)') !== null;
|
addButton.disabled = !hasSelectedAvailable ||
|
(config.limit > 0 && selectedItems.length >= config.limit);
|
|
// Remove button is enabled if at least one selected item is selected
|
const hasSelectedItems = selectedList.querySelector('li.selected') !== null;
|
removeButton.disabled = !hasSelectedItems;
|
}
|
|
// Initial search
|
performSearch();
|
})();
|
</script>
|
<?php
|
}
|
|
private function renderTrueFalseField(string $name, mixed $value, array $field):void
|
{
|
$conditional = $this->handleConditionalField($field);
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
?>
|
<div class="field true-false <?=$name?> row btw" <?=$conditional?> data-field="<?=$name?>">
|
<label class="toggle-switch row"<?=$describedBy?>>
|
<input
|
type="checkbox"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="1"
|
<?= ($value) ? ' checked':'' ?>
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
>
|
<div class="slider"></div>
|
<span class="toggle-label"><?= esc_html($field['label']); ?></span>
|
</label>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
}
|
|
private function renderUploadField(string $name, mixed $value, array $field): void
|
{
|
$defaultConfig = [
|
//File Type
|
'subtype' => 'image', // 'image', 'video', 'document', 'any'
|
'accepted_types' => null, // null = use subtype defaults, or array of specific MIME types
|
//Upload Behaviour
|
'multiple' => false, // Single or multiple uploads
|
'limit' => 0, // Max number of uploads (0 = unlimited)
|
'mode' => 'direct', // 'direct' or 'selection'
|
//Destination
|
'destination' => 'meta', // 'meta', 'post', 'post_group'
|
//Processing Options
|
'max_size' => null, // Override default size limits
|
'convert' => 'webp', // Image conversion format
|
'quality' => 80, // Conversion quality
|
'create_thumbnails' => true,
|
];
|
$config = array_merge($defaultConfig, $field);
|
|
// Validate destination config
|
if (in_array($config['destination'], ['post', 'post_group']) && empty($config['content'])) {
|
error_log("Upload field '{$name}' has destination '{$config['destination']}' but no content defined");
|
return;
|
}
|
|
// Get accepted types
|
$acceptedTypes = $this->getAllowedTypes($config);
|
|
// Build accept attribute for input
|
$acceptExtensions = $this->getMimeExtensions($acceptedTypes);
|
$acceptAttr = implode(',', $acceptExtensions);
|
|
// Determine field attributes
|
$subtype = $config['subtype'] ?? 'image';
|
$multiple = $config['multiple'] ?? false;
|
$limit = $config['limit'] ?? 0;
|
$mode = $config['mode'] ?? 'direct';
|
$destination = $config['destination'];
|
|
// Get existing attachments
|
$attachmentIds = $this->parseAttachmentIds($value);
|
|
// Determine field type for UI
|
$fieldType = $multiple ? 'gallery' : 'single';
|
|
// Build data attributes
|
$dataAttrs = [
|
'data-field' => $name,
|
'data-upload-field' => '',
|
'data-mode' => $mode,
|
'data-type' => $fieldType,
|
'data-subtype' => $subtype,
|
'data-destination' => $destination,
|
];
|
if (!empty($field['content'])) {
|
$dataAttrs['data-content'] = $field['content'];
|
}
|
if ($limit > 0) {
|
$dataAttrs['data-limit'] = $limit;
|
}
|
|
// Build data attributes
|
$conditional = $this->handleConditionalField($field);
|
$describedBy = !empty($field['description']) ? ' aria-describedby="' . esc_attr($name) . '-help"' : '';
|
|
if (!empty($field['group'])) {
|
$name = $field['group'] . '::' . $name;
|
}
|
|
// Convert data attributes to string
|
$dataAttrString = '';
|
foreach ($dataAttrs as $attr => $val) {
|
$dataAttrString .= ' ' . $attr . ($val !== '' ? '="' . esc_attr($val) . '"' : '');
|
}
|
?>
|
<div class="field upload <?= esc_attr($name) ?>"
|
<?= $dataAttrString ?>
|
<?= $conditional ?>>
|
|
<div class="file-upload-container">
|
<div class="file-upload-wrapper">
|
<input type="file"
|
name="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
|
id="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
|
accept="<?= esc_attr($acceptAttr) ?>"
|
data-max-size="<?= esc_attr($this->getMaxFileSize($subtype)) ?>"
|
<?= $multiple ? 'multiple' : '' ?>
|
<?= !empty($field['required']) ? 'required' : '' ?>>
|
|
<h2><?= esc_html($field['label']) ?></h2>
|
|
<?php if (!empty($field['description'])) : ?>
|
<p><?= esc_html($field['description']) ?></p>
|
<?php endif; ?>
|
|
<p class="file-upload-text">
|
<strong>Click to upload</strong> or drag and drop<br>
|
<?= esc_html($this->getAcceptedTypesLabel($subtype, $acceptExtensions)) ?>
|
(max. <?= esc_html($this->formatFileSize($this->getMaxFileSize($subtype))) ?>)
|
</p>
|
|
<?php if ($destination === 'post_group') {
|
$plural = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['plural'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['plural'] : str_replace('_', ' ',$field['content']).'s');
|
$singular = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['singular'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['singular'] : str_replace('_', ' ',$field['content']));
|
?>
|
<p class="hint">You can group images to create separate <?= $plural ?>.</p>
|
<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
|
<?php }
|
if (!empty($field['upload_description'])) : ?>
|
<p><?= esc_html($field['upload_description']); ?></p>
|
<?php endif; ?>
|
<div class="file-error"></div>
|
</div>
|
</div>
|
|
|
<?php if ($destination === 'post_group') : ?>
|
<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" 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">
|
<?= jvbIcon('add') ?>
|
Group
|
</button>
|
<button type="button" data-action="delete-upload">
|
<?= jvbIcon('delete') ?>
|
Delete
|
</button>
|
</div>
|
</div>
|
|
<button type="button" data-action="upload" class="submit-uploads">
|
<?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
|
</button>
|
</div>
|
<?php endif; ?>
|
|
<?php jvbRenderProgressBar('<span class="text">Processing files...</span>
|
<span class="count">0/0</span>'); ?>
|
<div class="item-grid preview">
|
<?php
|
// Render existing attachments
|
foreach ($attachmentIds as $attachmentId) {
|
echo $this->renderExistingAttachment($attachmentId, $subtype);
|
}
|
?>
|
</div>
|
|
<?php if ($destination === 'post_group') : ?>
|
<p class="hint"><?= jvbIcon('elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('elbow-right-up')?></p>
|
</div>
|
<div class="sidebar flex col">
|
<div class="header">
|
<h4>New <?= $plural?></h4>
|
<p class="hint">Drag or select multiple images into groups to create separate <?= $plural ?>.</p>
|
</div>
|
<div class="item-grid groups">
|
<div class="empty-group">
|
<p>Drag here to create a new <?= $singular ?>!</p>
|
</div>
|
</div>
|
<p class="hint"><?= jvbIcon('elbow-left-up') ?> Each group will become its own <?= $singular ?> <?= jvbIcon('elbow-right-up')?></p>
|
</div>
|
</div>
|
<?php endif; ?>
|
|
<?php if ($destination === 'meta') : ?>
|
<input type="hidden"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($value); ?>"
|
<?= !empty($field['required']) ? 'required' : ''; ?>>
|
<?php endif; ?>
|
</div>
|
<?php
|
}
|
|
|
protected function getAllowedTypes(array $config):array
|
{
|
$typeMap = [
|
'image' => [
|
'image/jpeg',
|
'image/png',
|
'image/gif',
|
'image/webp'
|
],
|
'video' => [
|
'video/mp4',
|
'video/webm',
|
'video/ogg',
|
'video/ogv',
|
'video/quicktime'
|
],
|
'document' => [
|
'application/pdf',
|
'application/msword',
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
'application/vnd.ms-excel',
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
'text/plain',
|
'text/csv'
|
],
|
'any' => [] // Will be merged from all types
|
];
|
// If specific types are defined, use those
|
if (!empty($config['accepted_types'])) {
|
return is_array($config['accepted_types'])
|
? $config['accepted_types']
|
: [$config['accepted_types']];
|
}
|
|
// Otherwise use subtype defaults
|
$subtype = $config['subtype'] ?? 'image';
|
|
if ($subtype === 'any') {
|
return array_merge(
|
$typeMap['image'],
|
$typeMap['video'],
|
$typeMap['document']
|
);
|
}
|
|
return $typeMap[$subtype] ?? $typeMap['image'];
|
|
}
|
/**
|
* Parse attachment IDs from value
|
*/
|
private function parseAttachmentIds(mixed $value): array
|
{
|
if (empty($value)) return [];
|
|
if (is_array($value)) {
|
return array_filter(array_map('absint', $value));
|
}
|
|
return array_filter(array_map('absint', explode(',', $value)));
|
}
|
|
/**
|
* Get file extensions for MIME types
|
*/
|
private function getMimeExtensions(array $mimeTypes): array
|
{
|
$extensionMap = [
|
'image/jpeg' => ['.jpg', '.jpeg'],
|
'image/png' => ['.png'],
|
'image/gif' => ['.gif'],
|
'image/webp' => ['.webp'],
|
'video/mp4' => ['.mp4'],
|
'video/webm' => ['.webm'],
|
'video/ogg' => ['.ogg'],
|
'video/ogv' => ['.ogv'],
|
'video/quicktime' => ['.mov'],
|
'application/pdf' => ['.pdf'],
|
'application/msword' => ['.doc'],
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['.docx'],
|
'application/vnd.ms-excel' => ['.xls'],
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['.xlsx'],
|
'text/plain' => ['.txt'],
|
'text/csv' => ['.csv'],
|
];
|
|
$extensions = [];
|
foreach ($mimeTypes as $mime) {
|
if (isset($extensionMap[$mime])) {
|
$extensions = array_merge($extensions, $extensionMap[$mime]);
|
}
|
}
|
|
return array_unique($extensions);
|
}
|
|
/**
|
* Get max file size for subtype
|
*/
|
private function getMaxFileSize(string $subtype): int
|
{
|
$sizes = [
|
'image' => 5242880, // 5MB
|
'video' => 104857600, // 100MB
|
'document' => 10485760 // 10MB
|
];
|
|
return $sizes[$subtype] ?? $sizes['image'];
|
}
|
/**
|
* Get human-readable file size label
|
*/
|
private function getMaxFileSizeLabel(string $subtype): string
|
{
|
$bytes = $this->getMaxFileSize($subtype);
|
$mb = round($bytes / 1048576);
|
return "{$mb}MB";
|
}
|
/**
|
* Format file size for display
|
*/
|
private function formatFileSize(int $bytes): string
|
{
|
if ($bytes >= 1073741824) {
|
return number_format($bytes / 1073741824, 1) . 'GB';
|
}
|
if ($bytes >= 1048576) {
|
return number_format($bytes / 1048576, 1) . 'MB';
|
}
|
if ($bytes >= 1024) {
|
return number_format($bytes / 1024, 1) . 'KB';
|
}
|
return $bytes . 'B';
|
}
|
|
/**
|
* Get accepted types label
|
*/
|
private function getAcceptedTypesLabel(string $subtype, array $extensions): string
|
{
|
$labels = [
|
'image' => 'JPG, PNG, GIF, or WEBP',
|
'video' => 'MP4, WEBM, or MOV',
|
'document' => 'PDF, DOC, XLS, or TXT',
|
'any' => 'Images, Videos, or Documents'
|
];
|
|
return $labels[$subtype] ?? strtoupper(implode(', ', array_map(function($ext) {
|
return ltrim($ext, '.');
|
}, array_slice($extensions, 0, 3))));
|
}
|
|
/**
|
* Render existing attachment
|
*/
|
private function renderExistingAttachment(int $attachmentId, string $subtype): string
|
{
|
$attachment = get_post($attachmentId);
|
if (!$attachment) return '';
|
|
$url = wp_get_attachment_url($attachmentId);
|
$thumbUrl = $subtype === 'image'
|
? wp_get_attachment_image_url($attachmentId, 'medium')
|
: $url;
|
|
ob_start();
|
?>
|
<div class="upload-item existing" data-attachment-id="<?= esc_attr($attachmentId) ?>" data-subtype="<?= esc_attr($subtype) ?>">
|
<div class="preview">
|
<?php if ($subtype === 'image') : ?>
|
<img src="<?= esc_url($thumbUrl) ?>" alt="<?= esc_attr(get_post_meta($attachmentId, '_wp_attachment_image_alt', true)) ?>">
|
<?php elseif ($subtype === 'video') : ?>
|
<video src="<?= esc_url($url) ?>" controls></video>
|
<?php else : ?>
|
<div class="document-preview">
|
<?= jvbIcon('document') ?>
|
<span><?= esc_html(basename($url)) ?></span>
|
</div>
|
<?php endif; ?>
|
|
<div class="overlay">
|
<div class="actions">
|
<button type="button" class="remove" title="Remove">
|
<span class="screen-reader-text">Remove <?= esc_attr($subtype) ?></span>
|
×
|
</button>
|
</div>
|
</div>
|
</div>
|
|
<?php if ($subtype === 'image') {
|
echo jvbImageMeta();
|
} ?>
|
</div>
|
<?php
|
return ob_get_clean();
|
}
|
|
private function renderImageField(string $name, mixed $value, array $field):void
|
{
|
$image_url = $title = $alt = $caption = false;
|
if ($value !== 0 || $value !== '') {
|
$image_url = wp_get_attachment_image_url((int)$value, 'medium') ?: false;
|
$caption = wp_get_attachment_caption((int)$value);
|
$alt = get_post_meta((int)$value, '_wp_attachment_image_alt', true);
|
$title = get_the_title((int)$value);
|
}
|
|
$mode = array_key_exists('mode', $field) ? $field['mode'] : 'direct';
|
$multiple = ($mode === 'selection' || isset($field['multiple']));
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
$groupable = (array_key_exists('imageType', $field) && $field['imageType'] === 'groupable');
|
$singular = (array_key_exists('singular', $field)) ? $field['singular'] : 'post';
|
$plural = (array_key_exists('plural', $field)) ? $field['plural'] : 'posts';
|
$dataContent = (array_key_exists('content', $field)) ? ' data-content="'.$field['content'].'"' : '';
|
$dataType = ($groupable) ? 'groupable' : (($multiple) ? 'gallery' : 'single');
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
?>
|
<div class="field image <?=$name?>"
|
data-field="<?= esc_attr($name); ?>"
|
data-upload-field
|
data-mode="<?= esc_attr($mode); ?>"
|
<?=$dataContent?>
|
<?= ' data-type="'.$dataType.'"'?>>
|
|
<div class="file-upload-container">
|
<div class="file-upload-wrapper">
|
<input type="file"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
|
accept=".jpg,.jpeg,.png,.gif,.webp"
|
data-max-size="<?= $this->max_file_size; ?>"
|
<?= $multiple ? 'multiple' : ''; ?>>
|
<h2><?= esc_html($field['label']); ?></h2>
|
<?php if (!empty($field['description'])) : ?>
|
<p><?= esc_html($field['description']); ?></p>
|
<?php endif; ?>
|
<p class="file-upload-text">
|
<strong>Click to upload</strong> or drag and drop<br>
|
JPG, PNG, GIF, or WEBP (max. 5MB)
|
</p>
|
<?php if ($groupable) { ?>
|
<p class="hint">You can group images to create separate <?= $plural ?>.</p>
|
<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
|
<?php } ?>
|
<?php if (!empty($field['upload_description'])) : ?>
|
<p><?= esc_html($field['upload_description']); ?></p>
|
<?php endif; ?>
|
</div>
|
<div class="file-error"></div>
|
</div>
|
<?php if ($groupable) : ?>
|
<div class="group-display" hidden>
|
<div class="preview-wrap">
|
<div class="preview-actions">
|
<div class="selection-controls">
|
<div class="selected">
|
<div class="field">
|
<input type="checkbox" id="select-all-uploads" name="select-all-uploads">
|
<label for="select-all-uploads">
|
Select All
|
</label>
|
</div>
|
<div class="info" hidden>
|
With <span class="selection-count">0</span> selected
|
</div>
|
</div>
|
|
|
<!-- Selection actions (hidden by default) -->
|
<div class="selection-actions" hidden>
|
<button type="button" class="create-from-selection">
|
<?= jvbIcon('add') ?>
|
Create New <?= $singular ?>
|
</button>
|
<button type="button" class="remove-selection">
|
<?= jvbIcon('delete') ?>
|
Remove
|
</button>
|
</div>
|
</div>
|
|
<button type="button" class="submit-uploads">
|
<?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
|
</button>
|
</div>
|
<?php endif; ?>
|
|
<?php jvbRenderProgressBar('<span class="text">Processing files...</span>
|
<span class="count">0/0</span>'); ?>
|
<div class="item-grid preview">
|
<?php if ($image_url) {
|
echo jvbRenderImageForm((int)$value);
|
} ?>
|
</div>
|
|
<?php if ($groupable) : ?>
|
<p class="hint"><?= jvbIcon('elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('elbow-right-up')?></p>
|
</div>
|
<div class="sidebar">
|
<div class="header">
|
<h4>New <?= $plural?></h4>
|
<p class="hint">Drag images into groups to create separate <?= $plural ?>.</p>
|
<p class="hint">Select multiple images and click "Add to <?= $singular?>" or create new <?= $plural ?>.</p>
|
</div>
|
<button type="button" class="create-group-from-selection">
|
<?= jvbIcon('add') ?>
|
Create New <?= $singular ?>
|
</button>
|
<div class="item-grid groups">
|
<div class="empty-group">
|
<p>Drag here to create a new <?= $singular ?>!</p>
|
</div>
|
</div>
|
<p class="hint"><?= jvbIcon('elbow-left-up') ?> Each group will become its own <?= $singular ?> <?= jvbIcon('elbow-right-up')?></p>
|
</div>
|
</div>
|
<?php endif; ?>
|
|
<?php if ($mode === 'direct') : ?>
|
<input type="hidden"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($value); ?>"
|
<?= !empty($field['required']) ? 'required' : ''; ?>>
|
<?php endif; ?>
|
</div>
|
<?php
|
}
|
|
protected function renderGalleryField(string $name, string|null|false $value, array $field):void
|
{
|
$ids = ($value === '' || is_null($value) || !$value) ? [] : explode(',',$value);
|
|
if (!empty($ids)) {
|
$ids = array_map('absint', $ids);
|
}
|
|
$conditional = $this->handleConditionalField($field);
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
//TODO: This can probably just be a wrapper for renderImageField...
|
?>
|
<div class="field gallery <?=$name?>"
|
data-field="<?= esc_attr($name); ?>"
|
<?= $conditional ?>>
|
|
<label><?= esc_html($field['label']); ?></label>
|
|
<!-- Container for existing images -->
|
<div class="gallery-preview">
|
<?php
|
if (!empty($ids)) {
|
foreach ($ids as $id) {
|
$url = wp_get_attachment_image_url($id, 'medium');
|
if ($url) {
|
echo '<div class="preview-item" data-id="' . esc_attr($id) . '">';
|
echo '<img src="' . esc_url($url) . '" alt="">';
|
echo '<button type="button" class="remove-preview">' . jvbIcon('trash', ['title'=>'Remove']) . '</button>';
|
echo '</div>';
|
}
|
}
|
}
|
?>
|
</div>
|
|
<!-- Hidden file uploader that will be managed by BatchFileUploader -->
|
<div class="file-upload-container">
|
<div class="file-upload-wrapper">
|
<input type="file"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
|
accept=".jpg,.jpeg,.png,.gif,.webp"
|
multiple>
|
<p class="file-upload-text">
|
<strong>Click to upload</strong> or drag and drop<br>
|
JPG, PNG, GIF, or WEBP (max. 5MB)
|
</p>
|
</div>
|
<div class="file-error"></div>
|
</div>
|
|
<!-- Hidden input for storing the IDs -->
|
<input type="hidden"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($value); ?>"
|
<?= !empty($field['required']) ? 'required' : ''; ?>>
|
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
|
<?php
|
}
|
private function renderSelectField(string $name, mixed $value, array $field):void
|
{
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
$conditional = $this->handleConditionalField($field);
|
$default = isset($field['default']) ? $field['default'] : '';
|
$value = !empty($value) ? $value : $default;
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
?>
|
<div class="field <?=$field['type']?> <?=$name?>" data-field="<?=$name?>" <?=$conditional?>>
|
<label for="<?= esc_attr($name); ?>">
|
<?= esc_html($field['label']); ?>
|
</label>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
<select
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
<?=$describedBy?>
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
>
|
<?php foreach ($field['options'] as $key => $label) : ?>
|
<option value="<?= esc_attr($key); ?>"
|
<?php selected($value, $key); ?>>
|
<?= esc_html($label); ?>
|
</option>
|
<?php endforeach; ?>
|
</select>
|
</div>
|
<?php
|
}
|
|
protected function renderHtmlField(string $name, mixed $value, array $field):void
|
{
|
$method_name = $field['content'];
|
$content = '';
|
if (method_exists($this, $method_name)) {
|
$content = $this->$method_name();
|
}
|
|
echo ($content == '') ? '' : sprintf(
|
'<div class="html-field-container" data-field-type="html" data-field="%s">%s</div>',
|
esc_attr($name),
|
$content
|
);
|
}
|
|
private function renderDateField(string $name, mixed $value, array $field):void
|
{
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
$conditional = $this->handleConditionalField($field);
|
$format = !empty($field['format']) ? $field['format'] : 'Y-m-d';
|
|
// Format the date if we have a value
|
if (!empty($value)) {
|
$date = DateTime::createFromFormat($format, $value);
|
if ($date) {
|
$value = $date->format('Y-m-d'); // HTML date input requires Y-m-d format
|
}
|
}
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
?>
|
<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
|
<label for="<?= esc_attr($name); ?>">
|
<?= esc_html($field['label']); ?>
|
</label>
|
<div class="date-wrapper"<?=$describedBy?>>
|
<input
|
type="date"
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($value); ?>"
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
data-format="<?= esc_attr($format); ?>"
|
>
|
<?= jvbIcon('event') ?>
|
</div>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
|
</div>
|
<?php
|
}
|
|
public function renderTimeField(string $name, mixed $value, array $field):void
|
{
|
$conditional = $this->handleConditionalField($field);
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
// Convert various time formats to HTML time input format (HH:MM)
|
if (!empty($value)) {
|
// If it's already in HH:MM format, use as-is
|
if (preg_match('/^\d{2}:\d{2}$/', $value)) {
|
// Value is already in correct format
|
} else {
|
// Try to parse and convert
|
$timestamp = strtotime($value);
|
if ($timestamp !== false) {
|
$value = date('H:i', $timestamp);
|
} else {
|
$value = '';
|
}
|
}
|
}
|
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
?>
|
<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
|
<label for="<?= esc_attr($name); ?>">
|
<?= esc_html($field['label']); ?>
|
</label>
|
<div class="time-wrapper"<?=$describedBy?>>
|
<input
|
type="time"
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($value); ?>"
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
<?= !empty($field['min']) ? 'min="' . esc_attr($field['min']) . '"' : ''; ?>
|
<?= !empty($field['max']) ? 'max="' . esc_attr($field['max']) . '"' : ''; ?>
|
<?= !empty($field['step']) ? 'step="' . esc_attr($field['step']) . '"' : ''; ?>
|
>
|
|
</div>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
}
|
|
private function renderDatetimeField(string $name, mixed $value, array $field):void
|
{
|
$conditional = $this->handleConditionalField($field);
|
$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
|
// Convert datetime to HTML datetime-local format (YYYY-MM-DDTHH:MM)
|
if (!empty($value)) {
|
$date = DateTime::createFromFormat('Y-m-d H:i:s', $value);
|
if (!$date) {
|
// Try alternative formats
|
$formats = ['Y-m-d\TH:i:s', 'Y-m-d\TH:i', 'Y-m-d H:i'];
|
foreach ($formats as $format) {
|
$date = DateTime::createFromFormat($format, $value);
|
if ($date) break;
|
}
|
}
|
|
if ($date) {
|
$value = $date->format('Y-m-d\TH:i'); // HTML datetime-local format
|
} else {
|
$value = '';
|
}
|
}
|
|
if (array_key_exists('group', $field)) {
|
$name = $field['group'].'::'.$name;
|
}
|
?>
|
<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
|
<label for="<?= esc_attr($name); ?>">
|
<?= esc_html($field['label']); ?>
|
</label>
|
<div class="datetime-wrapper"<?=$describedBy?>>
|
<input
|
type="datetime-local"
|
id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
|
value="<?= esc_attr($value); ?>"
|
<?= !empty($field['required']) ? 'required' : ''; ?>
|
<?= !empty($field['min']) ? 'min="' . esc_attr($field['min']) . '"' : ''; ?>
|
<?= !empty($field['max']) ? 'max="' . esc_attr($field['max']) . '"' : ''; ?>
|
<?= !empty($field['step']) ? 'step="' . esc_attr($field['step']) . '"' : ''; ?>
|
>
|
<?= jvbIcon('event') ?>
|
</div>
|
<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?>
|
<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?>
|
</div>
|
<?php
|
}
|
|
|
public function outputCharacterCountJS():void
|
{
|
?>
|
<script>
|
document.querySelectorAll('[maxlength]').forEach(field => {
|
const counter = field.closest('.field')?.querySelector('.char-count .current');
|
if (counter) {
|
const updateCount = () => counter.textContent = field.value.length;
|
field.addEventListener('input', updateCount);
|
updateCount();
|
}
|
});
|
</script>
|
<?php
|
}
|
|
|
//Conditional Fields
|
private function handleConditionalField(array $field):string
|
{
|
if (empty($field['condition'])) {
|
return '';
|
}
|
|
$condition = $field['condition'];
|
return sprintf(
|
'data-depends-on="%s" data-depends-value="%s" data-depends-operator="%s"',
|
esc_attr($field['condition']['field']),
|
esc_attr($field['condition']['value']),
|
esc_attr($field['condition']['operator'] ?? '==')
|
);
|
}
|
|
protected function renderDescription(string $description, string $name):void
|
{
|
$id = $name.'-help';
|
$out = '<div class="has-tooltip">
|
<span class="tt-toggle">'.jvbIcon('help').'</span>
|
<div role="tooltip" id="'.$id.'"><p>'.$description.'</p></div>
|
</div>';
|
echo $out;
|
}
|
|
protected function renderHint(array|string $hint):void
|
{
|
if (is_array($hint)) {
|
$out = '';
|
foreach($hint as $h) {
|
$out .= '<p class="hint">'.$h.'</p>';
|
}
|
} else {
|
$out = '<p class="hint">'.$hint.'</p>';
|
}
|
echo $out;
|
}
|
}
|