| | |
| | | class MetaForm |
| | | { |
| | | protected int $max_file_size = 5242880; |
| | | protected ?MetaTypeManager $type_manager = null; |
| | | |
| | | /* ========== MAIN RENDER METHOD ========== */ |
| | | public function return(string $name, mixed $value, array $config, bool $showHidden = false) |
| | |
| | | public function render(string $name, mixed $value, array $config, bool $showHidden = false, bool $return = false): mixed |
| | | { |
| | | $out = ''; |
| | | |
| | | if (jvbCheck('hidden', $config) && !$showHidden) { |
| | | return $out; |
| | | } |
| | |
| | | if (!array_key_exists('type', $config)) { |
| | | return $out; |
| | | } |
| | | if (!$value) { |
| | | $value = $this->getDefaultValue($config['type']); |
| | | } |
| | | |
| | | // Handle hidden display type |
| | | if (array_key_exists('display', $config) && $config['display'] === 'hidden') { |
| | |
| | | return $out; |
| | | } |
| | | |
| | | public function getDefaultValue(string $type):mixed { |
| | | if (!$this->type_manager) { |
| | | $this->type_manager = new MetaTypeManager(); |
| | | } |
| | | return match ($this->type_manager->getMetaType($type)) { |
| | | 'object', 'array' => [], |
| | | 'boolean' => false, |
| | | 'integer' => 0, |
| | | default => '', |
| | | }; |
| | | } |
| | | |
| | | /* ========== HELPER METHODS ========== */ |
| | | |
| | | /** |
| | |
| | | if (!empty($field['validation_message'])) { |
| | | $attrs['data-validation-message'] = $field['validation_message']; |
| | | } |
| | | |
| | | $attrs['data-type'] = $field['type']; |
| | | |
| | | $attrString = ''; |
| | |
| | | $validationAttrs = $this->buildValidationAttributes($field); |
| | | $conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : ''; |
| | | |
| | | $customData = ''; |
| | | if (array_key_exists('data', $field) && !empty($field['data'])) { |
| | | foreach ($field['data'] as $key => $v) { |
| | | $customData .= ($v === '') ? ' data-' . $key : ' data-' . $key . '="' . $v . '"'; |
| | | } |
| | | } |
| | | ?> |
| | | <div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>" |
| | | <?= $conditional ?> |
| | |
| | | name="<?= esc_attr($data['name']) ?>" |
| | | value="<?= esc_attr($data['value']) ?>" |
| | | <?= $inputAttrs ?> |
| | | <?= $customData?> |
| | | > |
| | | <span class="validation-icon success" hidden aria-hidden="true"> |
| | | <?= jvbIcon('check-circle') ?> |
| | |
| | | <?php |
| | | } |
| | | |
| | | protected function renderComplexFieldWrapper(string $name, array $field, callable $renderContent): void |
| | | { |
| | | $data = $this->prepareFieldData($name, $field['value'] ?? '', $field); |
| | | $validationAttrs = $this->buildValidationAttributes($field); |
| | | $conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : ''; |
| | | $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; |
| | | |
| | | // Additional data attributes for complex fields |
| | | $dataAttrs = ''; |
| | | if (array_key_exists('data', $field) && !empty($field['data'])) { |
| | | foreach ($field['data'] as $key => $val) { |
| | | $dataAttrs .= ($val === '') ? ' data-' . $key : ' data-' . $key . '="' . esc_attr($val) . '"'; |
| | | } |
| | | } |
| | | |
| | | ?> |
| | | <div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>" |
| | | <?= $conditional ?> |
| | | data-field="<?= esc_attr($name) ?>" |
| | | <?= $validationAttrs ?> |
| | | <?= $dataAttrs ?> |
| | | <?= $describedBy ?>> |
| | | |
| | | <?php if (!empty($field['label']) && (!isset($field['show_label']) || $field['show_label'])) : ?> |
| | | <h3 class="field-label"><?= esc_html($field['label']) ?></h3> |
| | | <?php endif; ?> |
| | | |
| | | <?php $this->renderHintAndDescription($field, $name); ?> |
| | | |
| | | <div class="field-content"> |
| | | <?php $renderContent($name, $data, $field); ?> |
| | | </div> |
| | | |
| | | <span class="validation-message" hidden role="alert"></span> |
| | | </div> |
| | | <?php |
| | | } |
| | | |
| | | /** |
| | | * Render field label with optional character count |
| | | */ |
| | |
| | | */ |
| | | protected function renderHintAndDescription(array $field, string $name): void |
| | | { |
| | | if (array_key_exists('hint', $field)) { |
| | | if (!empty($field['hint'])) { |
| | | $this->renderHint($field['hint']); |
| | | } |
| | | |
| | | if (array_key_exists('description', $field)) { |
| | | if (!empty($field['description'])) { |
| | | $this->renderDescription($field['description'], $name); |
| | | } |
| | | } |
| | | |
| | | protected function renderHint(string $hint): void |
| | | { |
| | | ?> |
| | | <span class="hint"><?= esc_html($hint) ?></span> |
| | | <?php |
| | | } |
| | | |
| | | protected function renderDescription(string $description, string $name): void |
| | | { |
| | | ?> |
| | | <p class="description" id="<?= esc_attr($name) ?>-help"> |
| | | <?= wp_kses_post($description) ?> |
| | | </p> |
| | | <?php |
| | | } |
| | | |
| | | /* ========== SIMPLE INPUT FIELD TYPES ========== */ |
| | | |
| | | public function renderTextField(string $name, mixed $value, array $field): void |
| | | { |
| | | $this->renderStandardInput($name, $value, $field, $field['input_type'] ?? 'text'); |
| | | $this->renderStandardInput($name, $value, $field, $field['subtype'] ?? 'text'); |
| | | } |
| | | |
| | | public function renderEmailField(string $name, mixed $value, array $field): void |
| | |
| | | class="decrease" |
| | | title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>" |
| | | aria-label="Decrease <?= esc_attr($field['label']) ?>"> |
| | | <?= jvbIcon('minus') ?> |
| | | <?= jvbIcon('minus-square') ?> |
| | | </button> |
| | | |
| | | <input type="number" |
| | |
| | | class="increase" |
| | | title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>" |
| | | aria-label="Increase <?= esc_attr($field['label']) ?>"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | </button> |
| | | </div> |
| | | |
| | |
| | | <select |
| | | id="<?= esc_attr($data['id']) ?>" |
| | | name="<?= esc_attr($data['name']) ?>" |
| | | <?= $inputAttrs ?> |
| | | > |
| | | <?= $inputAttrs ?>> |
| | | <?php foreach ($field['options'] as $key => $label) : ?> |
| | | <option value="<?= esc_attr($key) ?>" <?php selected($value, $key); ?>> |
| | | <?= esc_html($label) ?> |
| | |
| | | </legend> |
| | | |
| | | <?php foreach ($field['options'] as $key => $label) : ?> |
| | | <label class="radio-option"> |
| | | <input |
| | | type="radio" |
| | | name="<?= esc_attr($data['name']) ?>" |
| | | value="<?= esc_attr($key) ?>" |
| | | <?php checked($value, $key); ?> |
| | | <?= !empty($field['required']) ? 'required' : '' ?> |
| | | > |
| | | <span><?= esc_html($label) ?></span> |
| | | <input |
| | | type="radio" |
| | | id="<?= esc_attr($data['name']) ?>-<?= esc_attr($key)?>" |
| | | name="<?= esc_attr($data['name']) ?>" |
| | | value="<?= esc_attr($key) ?>" |
| | | <?php checked($value, $key); ?> |
| | | <?= !empty($field['required']) ? 'required' : '' ?> |
| | | > |
| | | <label class="radio-option" for="<?= esc_attr($data['name']) ?>-<?= esc_attr($key)?>"> |
| | | <span><?= $label ?></span> |
| | | </label> |
| | | <?php endforeach; ?> |
| | | </fieldset> |
| | |
| | | |
| | | /* ========== REPEATER FIELD ========== */ |
| | | |
| | | private function renderRepeaterField(string $name, mixed $value, array $field): void |
| | | private function renderRepeaterField(string $name, mixed $value, array $field):void |
| | | { |
| | | $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; |
| | | $name = $field['group'].'::'.$name; |
| | | } |
| | | |
| | | $this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use ($value) { |
| | | $values = is_array($value) ? $value : []; |
| | | $rowLabel = $field['row_label'] ?? ''; |
| | | $rowTitle = $field['new_row'] ?? 'New Item'; |
| | | $addLabel = $field['add_label'] ?? 'Add Item'; |
| | | |
| | | $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)); |
| | | } |
| | | ?> |
| | | <div class="repeater-items" data-label="<?= esc_attr($rowLabel) ?>"> |
| | | <h3><?= esc_html($field['label']); ?></h3> |
| | | |
| | | |
| | | <div class="repeater-items"> |
| | | <?php |
| | | if (!empty($values)) { |
| | | foreach ($values as $index => $row) { |
| | |
| | | ?> |
| | | </div> |
| | | |
| | | <template class="<?= uniqid('repeaterTemplate') ?>"> |
| | | <?php $this->renderRepeaterRow($field['fields'], [], '', $name, $rowTitle); ?> |
| | | <template class="<?=uniqid('repeaterTemplate')?>"> |
| | | <?php $this->renderRepeaterRow($field['fields'], array(), '', '', $rowTitle); ?> |
| | | </template> |
| | | |
| | | <button type="button" class="add-repeater-row button secondary"> |
| | | <?= jvbIcon('plus', ['title' => 'Add']) ?> |
| | | <span><?= esc_html($addLabel) ?></span> |
| | | <button type="button" class="add-repeater-row"> |
| | | <?= jvbIcon('plus', ['title'=> 'Add']); ?> <?= (array_key_exists('add_label', $field)) ? $field['add_label'] : 'Add Item'; ?> |
| | | </button> |
| | | <?php |
| | | }); |
| | | <?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): void |
| | | 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); |
| | | $display_number = (is_string($index)) ? $index : ($index + 1); |
| | | ?> |
| | | <div class="repeater-row" data-index="<?= esc_attr($index) ?>"> |
| | | <details <?= is_string($index) ? 'open' : '' ?>> |
| | | <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> |
| | | <span class="drag-handle"><?= jvbIcon('dots-six-vertical'); ?></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']) ?> |
| | | <?= jvbIcon('trash', ['title'=>'Remove']); ?> |
| | | </button> |
| | | </summary> |
| | | <div class="repeater-row-content"> |
| | | <?php |
| | | foreach ($fields as $slug => $field) { |
| | | $field_name = ($base_name === '') ? $slug : sprintf('%s:%s:%s', $base_name, $index, $slug); |
| | | $field_value = $values[$slug] ?? ''; |
| | | $this->render($field_name, $field_value, $field); |
| | | } |
| | | 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> |
| | |
| | | protected function renderGroupField(string $name, mixed $value, array $field): void |
| | | { |
| | | if (!array_key_exists('fields', $field) || empty($field['fields'])) { |
| | | error_log('No fields to render'); |
| | | return; |
| | | } |
| | | |
| | | |
| | | $values = is_array($value) ? $value : []; |
| | | $original = $name; |
| | | |
| | |
| | | $conditional = $this->handleConditionalField($field); |
| | | $validationAttrs = $this->buildValidationAttributes($field); |
| | | $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; |
| | | |
| | | $fieldset = (array_key_exists('wrap', $field) && $field['wrap'] === 'details') ? 'details' : 'fieldset'; |
| | | $legend = (array_key_exists('wrap', $field) && $field['wrap'] === 'details') ? 'summary' : 'legend'; |
| | | ?> |
| | | <fieldset class="field group <?= esc_attr($name) ?>" |
| | | <<?= $fieldset?> class="field group <?= esc_attr($name) ?>" |
| | | <?= $conditional ?> |
| | | data-field="<?= esc_attr($name) ?>" |
| | | <?= $validationAttrs ?> |
| | | <?= $describedBy ?>> |
| | | <legend><?= esc_html($field['label']) ?></legend> |
| | | <<?=$legend?>><?= esc_html($field['label']) ?></<?=$legend?>> |
| | | |
| | | <?php $this->renderHintAndDescription($field, $name); ?> |
| | | |
| | |
| | | </div> |
| | | |
| | | <span class="validation-message" hidden role="alert"></span> |
| | | </fieldset> |
| | | </<?= $fieldset?>> |
| | | <?php |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /* ========== UPLOAD FIELD ========== */ |
| | | |
| | | private function renderGalleryField(string $name, mixed $value, array $field):void |
| | | { |
| | | $field['multiple'] = true; |
| | | $this->renderUploadField($name, $value, $field); |
| | | } |
| | | private function renderUploadField(string $name, mixed $value, array $field): void |
| | | { |
| | | // Merge with defaults |
| | | $config = array_merge([ |
| | | 'subtype' => 'image', |
| | | 'accepted_types' => null, |
| | | 'multiple' => false, |
| | | 'limit' => 0, |
| | | 'mode' => 'direct', |
| | | 'destination' => 'meta', |
| | | 'max_size' => null, |
| | | 'convert' => 'webp', |
| | | 'quality' => 80, |
| | | $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' => 90, // Conversion quality |
| | | 'create_thumbnails' => true, |
| | | ], $field); |
| | | ]; |
| | | $config = array_merge($defaultConfig, $field); |
| | | |
| | | // Validate destination config |
| | | if (in_array($config['destination'], ['post', 'post_group']) && empty($config['content'])) { |
| | |
| | | return; |
| | | } |
| | | |
| | | if (array_key_exists('group', $field)) { |
| | | // 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; |
| | | } |
| | | |
| | | // Prepare upload configuration |
| | | $acceptedTypes = $this->getAllowedTypes($config); |
| | | $acceptExtensions = $this->getMimeExtensions($acceptedTypes); |
| | | $acceptAttr = implode(',', $acceptExtensions); |
| | | $attachmentIds = $this->parseAttachmentIds($value); |
| | | $fieldType = $config['multiple'] ? 'gallery' : 'image'; |
| | | |
| | | // Build data attributes for uploader.js |
| | | $uploadData = [ |
| | | 'data-subtype' => $config['subtype'], |
| | | 'data-mode' => $config['mode'], |
| | | 'data-destination' => $config['destination'], |
| | | 'data-multiple' => $config['multiple'] ? 'true' : 'false', |
| | | 'data-limit' => $config['limit'], |
| | | 'data-convert' => $config['convert'], |
| | | 'data-quality' => $config['quality'], |
| | | ]; |
| | | |
| | | if (!empty($config['content'])) { |
| | | $uploadData['data-content'] = $config['content']; |
| | | // 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 ?>> |
| | | |
| | | $this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use ( |
| | | $config, $acceptAttr, $attachmentIds, $fieldType, $uploadData, $value |
| | | ) { |
| | | ?> |
| | | <div class="upload-field-wrapper <?= esc_attr($fieldType) ?>" |
| | | <?php foreach ($uploadData as $attr => $val) : ?> |
| | | <?= $attr ?>="<?= esc_attr($val) ?>" |
| | | <?php endforeach; ?>> |
| | | |
| | | <!-- Preview Area --> |
| | | <div class="upload-preview-area"> |
| | | <?php $this->renderUploadPreviews($attachmentIds, $config); ?> |
| | | </div> |
| | | |
| | | <!-- Upload Area --> |
| | | <div class="file-upload-container"> |
| | | <div class="file-upload-wrapper"> |
| | | <input type="file" |
| | | name="<?= esc_attr($data['name']) ?>_temp" |
| | | 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) ?>" |
| | | <?= $config['multiple'] ? 'multiple' : '' ?>> |
| | | 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->getUploadInstructions($config)) ?> |
| | | <?= 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 class="file-error"></div> |
| | | <?php jvbRenderProgressBar(); ?> |
| | | </div> |
| | | |
| | | <!-- Hidden input for storing the IDs --> |
| | | <input type="hidden" |
| | | name="<?= esc_attr($data['name']) ?>" |
| | | value="<?= esc_attr($value) ?>" |
| | | <?= !empty($field['required']) ? 'required' : '' ?>> |
| | | |
| | | <?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" 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"> |
| | | <?= jvbIcon('plus-square') ?> |
| | | Group |
| | | </button> |
| | | <button type="button" data-action="delete-upload"> |
| | | <?= jvbIcon('trash') ?> |
| | | Delete |
| | | </button> |
| | | </div> |
| | | </div> |
| | | |
| | | <button type="button" data-action="upload" class="submit-uploads"> |
| | | <?= jvbIcon('cloud-arrow-up') ?> 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('arrow-elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('arrow-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('arrow-elbow-left-up') ?> Each group will become its own <?= $singular ?> <?= jvbIcon('arrow-elbow-right-up')?></p> |
| | | </div> |
| | | </div> |
| | | <?php |
| | | }); |
| | | <?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 |
| | | } |
| | | |
| | | private function renderExistingAttachment(int $attachmentId, string $subtype): string |
| | | { |
| | | ob_start(); |
| | | |
| | | switch ($subtype) { |
| | | case 'image': |
| | | $this->renderImagePreview($attachmentId); |
| | | break; |
| | | case 'video': |
| | | $this->renderVideoPreview($attachmentId); |
| | | break; |
| | | case 'document': |
| | | case 'file': |
| | | $this->renderFilePreview($attachmentId); |
| | | break; |
| | | default: |
| | | $this->renderImagePreview($attachmentId); |
| | | break; |
| | | } |
| | | |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | /** |
| | | * 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']; |
| | | } |
| | | |
| | | /** |
| | | * 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)))); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | foreach ($attachmentIds as $id) { |
| | | if ($config['subtype'] === 'image') { |
| | | $url = wp_get_attachment_image_url($id, 'thumbnail'); |
| | | if ($url) { |
| | | echo '<div class="upload-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>'; |
| | | } |
| | | switch ($config['subtype']) { |
| | | case 'image': |
| | | $this->renderImagePreview($id, $config); |
| | | break; |
| | | case 'video': |
| | | $this->renderVideoPreview($id, $config); |
| | | break; |
| | | case 'file': |
| | | $this->renderFilePreview($id, $config); |
| | | break; |
| | | } |
| | | // Add other subtypes (video, document) as needed |
| | | } |
| | | } |
| | | |
| | | public function renderImagePreview(?int $id = null, array $config = []):void |
| | | { |
| | | $attachment = ($id) ? wp_get_attachment_image($id, 'thumbnail', false) : false; |
| | | $caption = ($id) ? wp_get_attachment_caption($id) : ''; |
| | | $alt = ($id) ? get_post_meta($id, '_wp_attachment_image_alt',true) : ''; |
| | | $title = ($id) ? get_the_title($id) : ''; |
| | | $addID = ($id) ? '-'.$id : ''; |
| | | $dataID = ($id) ? ['id' => $id] : ''; |
| | | ?> |
| | | <div class="item upload"<?= ($id) ? ' data-id="'.$id.'"' : '' ?>> |
| | | <div class="preview"> |
| | | <?php jvbRenderProgressBar('',true) ?> |
| | | <input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>"> |
| | | <label for="select-item<?=$addID?>" aria-label="Select image"> |
| | | <?= ($attachment) ?: '<img> |
| | | <video></video> |
| | | <span></span>' ?> |
| | | </label> |
| | | <div class="item-actions row btw"> |
| | | <div class="radio-button"> |
| | | <input type="radio" class="featured btn" name="featured" id="featured" hidden> |
| | | <label for="featured"> |
| | | <?=jvbIcon('star')?> |
| | | <?=jvbIcon('star', ['style' => 'fill'])?> |
| | | <span class="screen-reader-text">Set as featured image</span> |
| | | </label> |
| | | </div> |
| | | |
| | | <button type="button" data-action="delete-upload" title="Remove from Group"> |
| | | <?=jvbIcon('trash')?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | <details> |
| | | <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary> |
| | | |
| | | <?php |
| | | |
| | | $fields = array_key_exists('fields', $config) ? $config['fields'] : []; |
| | | $fields = array_merge([ |
| | | 'upload_data' => [ |
| | | 'type' => 'group', |
| | | 'wrap' => 'details', |
| | | 'label' => 'Image Info', |
| | | 'hint' => 'These will be automatically generated if left blank.', |
| | | 'fields' => [ |
| | | 'image-title'.$addID => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Image Title', |
| | | 'value' => $title, |
| | | 'data' => $dataID |
| | | ], |
| | | 'image-alt-text'.$addID => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Alt Text', |
| | | 'value' => $alt, |
| | | 'hint' => 'Alt text helps the visually impaired, as well as some benefits for SEO.', |
| | | 'data' => $dataID |
| | | ], |
| | | 'image-caption'.$addID => [ |
| | | 'type' => 'textarea', |
| | | 'value' => $caption, |
| | | 'label' => 'Image Caption', |
| | | 'data' => $dataID |
| | | ] |
| | | ] |
| | | ] |
| | | ], $fields); |
| | | |
| | | $meta = new MetaManager($id); |
| | | foreach ($fields as $field => $config) { |
| | | $meta->render('form', $field, $config); |
| | | } |
| | | ?> |
| | | </details> |
| | | </div> |
| | | <?php |
| | | } |
| | | public function renderVideoPreview(?int $id = null, array $config = []):void |
| | | { |
| | | $attachment = ($id) ? wp_get_attachment_image($id, 'thumbnail', true) : false; |
| | | $caption = ($id) ? wp_get_attachment_caption($id) : ''; |
| | | $description = ($id) ? get_the_content($id) : ''; |
| | | $title = ($id) ? get_the_title($id) : ''; |
| | | $addID = ($id) ? '-'.$id : ''; |
| | | $dataID = ($id) ? ['id' => $id] : ''; |
| | | ?> |
| | | <div class="item upload"<?= ($id) ? ' data-id="'.$id.'"' : '' ?>> |
| | | <div class="preview"> |
| | | <?php jvbRenderProgressBar('',true) ?> |
| | | <input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>"> |
| | | <label for="select-item<?=$addID?>" aria-label="Select image"> |
| | | <?= ($attachment) ? $attachment : '<img> |
| | | <video></video> |
| | | <span></span>'; ?> |
| | | </label> |
| | | <div class="item-actions row btw"> |
| | | <div class="radio-button"> |
| | | <input type="radio" class="featured btn" name="featured" id="featured" hidden> |
| | | <label for="featured"> |
| | | <?=jvbIcon('star')?> |
| | | <?=jvbIcon('star', ['style' => 'fill'])?> |
| | | <span class="screen-reader-text">Set as featured image</span> |
| | | </label> |
| | | </div> |
| | | |
| | | <button type="button" data-action="delete-upload" title="Remove from Group"> |
| | | <?=jvbIcon('trash')?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | <details>'; |
| | | <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary> |
| | | |
| | | <?php |
| | | $fields = array_key_exists('fields', $config) ? $config['fields'] : []; |
| | | $fields = array_merge([ |
| | | 'upload_data' => [ |
| | | 'type' => 'group', |
| | | 'wrap' => 'details', |
| | | 'label' => 'Video Info', |
| | | 'hint' => 'These will be automatically generated if left blank.', |
| | | 'fields' => [ |
| | | 'title' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Video Title', |
| | | 'value' => $title, |
| | | 'data' => $dataID |
| | | ], |
| | | 'caption' => [ |
| | | 'type' => 'textarea', |
| | | 'value' => $caption, |
| | | 'label' => 'Video Caption', |
| | | 'data' => $dataID |
| | | ], |
| | | 'description' => [ |
| | | 'type' => 'textarea', |
| | | 'value' => $description, |
| | | 'label' => 'Video Description', |
| | | 'data' => $dataID |
| | | ] |
| | | ] |
| | | ] |
| | | ], $fields); |
| | | $this->render('upload_data', null, $fields); |
| | | ?> |
| | | </details> |
| | | </div> |
| | | <?php |
| | | } |
| | | public function renderFilePreview(?int $id = null, array $config = []):void |
| | | { |
| | | |
| | | $attachment = ($id) ? wp_get_attachment_image($id, 'thumbnail', true) : false; |
| | | $caption = ($id) ? wp_get_attachment_caption($id) : ''; |
| | | $description = ($id) ? get_the_content($id) : ''; |
| | | $title = ($id) ? get_the_title($id) : ''; |
| | | $addID = ($id) ? '-'.$id : ''; |
| | | $dataID = ($id) ? ['id' => $id] : ''; |
| | | ?> |
| | | <div class="item upload"<?= ($id) ? ' data-id="'.$id.'"' : '' ?>> |
| | | <div class="preview"> |
| | | <?php jvbRenderProgressBar('',true) ?> |
| | | <input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>"> |
| | | <label for="select-item<?=$addID?>" aria-label="Select image"> |
| | | <?= ($attachment) ? $attachment : '<img> |
| | | <video></video> |
| | | <span></span>'; ?> |
| | | </label> |
| | | <div class="item-actions row btw"> |
| | | <div class="radio-button"> |
| | | <input type="radio" class="featured btn" name="featured" id="featured" hidden> |
| | | <label for="featured"> |
| | | <?=jvbIcon('star')?> |
| | | <?=jvbIcon('star', ['style' => 'fill'])?> |
| | | <span class="screen-reader-text">Set as featured image</span> |
| | | </label> |
| | | </div> |
| | | |
| | | <button type="button" data-action="delete-upload" title="Remove from Group"> |
| | | <?=jvbIcon('trash')?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | <details>'; |
| | | <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary> |
| | | |
| | | <?php |
| | | $fields = array_key_exists('fields', $config) ? $config['fields'] : []; |
| | | $fields = array_merge([ |
| | | 'upload_data' => [ |
| | | 'type' => 'group', |
| | | 'wrap' => 'details', |
| | | 'label' => 'File Info', |
| | | 'hint' => 'These will be automatically generated if left blank.', |
| | | 'fields' => [ |
| | | 'title' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'File Title', |
| | | 'value' => $title, |
| | | 'data' => $dataID |
| | | ], |
| | | 'caption' => [ |
| | | 'type' => 'textarea', |
| | | 'value' => $caption, |
| | | 'label' => 'File Caption', |
| | | 'data' => $dataID |
| | | ], |
| | | 'description' => [ |
| | | 'type' => 'textarea', |
| | | 'value' => $description, |
| | | 'label' => 'File Description', |
| | | 'data' => $dataID |
| | | ] |
| | | ] |
| | | ] |
| | | ], $fields); |
| | | $this->render('upload_data', null, $fields); |
| | | ?> |
| | | </details> |
| | | </div> |
| | | <?php |
| | | } |
| | | |
| | | /** |
| | | * Get upload instruction text based on config |
| | | */ |
| | |
| | | |
| | | /* ========== TAXONOMY/USER SELECTOR FIELDS ========== */ |
| | | |
| | | private function renderTaxonomyField(string $name, mixed $value, array $field): void |
| | | private function renderTaxonomyField(string $name, string $value, array $field): void |
| | | { |
| | | if (array_key_exists('group', $field)) { |
| | | $name = $field['group'] . '::' . $name; |
| | |
| | | $this->renderSelectorField($name, $value, $field, 'taxonomy'); |
| | | } |
| | | |
| | | private function renderUserField(string $name, mixed $value, array $field): void |
| | | private function renderUserField(string $name, string $value, array $field): void |
| | | { |
| | | if (array_key_exists('group', $field)) { |
| | | $name = $field['group'] . '::' . $name; |
| | |
| | | * Generic selector field renderer |
| | | * Handles both taxonomy and post selectors with consistent structure |
| | | */ |
| | | private function renderSelectorField(string $name, mixed $value, array $field, string $type): void |
| | | public function renderSelectorField(string $name, mixed $value, array $field, string $type): void |
| | | { |
| | | $conditional = $this->handleConditionalField($field); |
| | | $validationAttrs = $this->buildValidationAttributes($field); |
| | | $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; |
| | | |
| | | // Parse selected values |
| | | $value = (is_array($value)) ? array_filter(array_map('absint', $value)): $value; |
| | | $selected = ($value === '') ? [] : (is_array($value) ? $value : explode(',', $value)); |
| | | |
| | | // Create selector instance |
| | | // Generate unique container ID |
| | | $containerId = $name . '-' . $type . '-selector'; |
| | | |
| | | // Create selector instance with proper parameters |
| | | if ($type === 'taxonomy') { |
| | | $taxonomy = $field['taxonomy']; |
| | | $icon = JVB_TAXONOMY[$taxonomy]['icon']??''; |
| | | |
| | | // Map field config to selector config |
| | | $selectorConfig = [ |
| | | 'multiple' => $field['multiple'] ?? true, |
| | | 'placeholder' => $field['placeholder'] ?? 'Search terms...', |
| | | 'noResults' => 'No terms found', |
| | | 'onClose' => 'updateMetaFormTaxonomy' |
| | | 'max' => $field['max'] ?? 0, // 0 = unlimited |
| | | 'search' => $field['search'] ?? true, |
| | | 'label' => $field['label'] ?? '', |
| | | 'createNew' => $field['createNew'] ?? false, |
| | | 'required' => $field['required'] ?? false, |
| | | 'base' => $field['base'] ?? '', |
| | | 'update' => $field['update'] ?? true, |
| | | 'name' => $name, |
| | | 'autocomplete' => $field['autocomplete'] ?? false, |
| | | ]; |
| | | $selector = new TaxonomySelector($taxonomy, $selectorConfig); |
| | | if ($icon !== '') { |
| | | $selectorConfig['icon'] = $icon; |
| | | } |
| | | |
| | | $selector = new TaxonomySelector($containerId, $taxonomy, $selectorConfig); |
| | | $icon = $taxonomy; |
| | | } else { |
| | | $postType = $field['post_type']; |
| | | |
| | | // Map field config to selector config |
| | | $selectorConfig = [ |
| | | 'multiple' => $field['multiple'] ?? true, |
| | | 'placeholder' => $field['placeholder'] ?? 'Search posts...', |
| | | 'noResults' => 'No posts found', |
| | | 'shop_id' => $field['shop_id'] ?? null, |
| | | 'onClose' => 'updateMetaFormPost' |
| | | 'max' => $field['max'] ?? 0, |
| | | 'search' => $field['search'] ?? true, |
| | | 'label' => $field['label'] ?? '', |
| | | 'required' => $field['required'] ?? false, |
| | | 'base' => $field['base'] ?? '', |
| | | 'update' => $field['update'] ?? true, |
| | | 'shop_id' => $field['shop_id'] ?? null, |
| | | 'autocomplete'=> $field['autocomplete'] ?? true, |
| | | ]; |
| | | $selector = new PostSelector($postType, $selectorConfig); |
| | | |
| | | $selector = new PostSelector($containerId, $postType, $selectorConfig); |
| | | $icon = $postType; |
| | | } |
| | | |
| | | $containerId = $name . '-' . $type . '-selector'; |
| | | |
| | | ?> |
| | | <div class="field <?= esc_attr($type) ?>-selector <?= esc_attr($name) ?>" |
| | | <div class="field selector <?= esc_attr($type) ?> <?= esc_attr($name) ?>" |
| | | <?= $conditional ?> |
| | | data-field="<?= esc_attr($name) ?>" |
| | | data-type="selector" data-subtype="<?= esc_attr($type)?>" |
| | | <?= $validationAttrs ?> |
| | | <?= $describedBy ?>> |
| | | |
| | | <div class="field-group-header row btw"> |
| | | <label class="toggle row"> |
| | | <?= jvbIcon($icon) ?> |
| | | <span><?= esc_html($field['label'] ?? ucfirst($type)) ?></span> |
| | | </label> |
| | | <button type="button" |
| | | class="add-item-btn button secondary" |
| | | title="Add <?= esc_attr(ucfirst($type)) ?>"> |
| | | <?= jvbIcon('add', ['title' => 'Add ' . ucfirst($type)]) ?> |
| | | </button> |
| | | </div> |
| | | |
| | | <?= $selector->render($selected, $containerId) ?> |
| | | <?= $selector->render($selected) ?> |
| | | |
| | | <!-- Hidden input for form submission --> |
| | | <input type="hidden" |
| | |
| | | return; |
| | | } |
| | | |
| | | // Parse stored data |
| | | // Extract stored values |
| | | if (is_string($value)) { |
| | | $value = maybe_unserialize($value); |
| | | } |
| | |
| | | $address = $stored_data['address'] ?? ''; |
| | | $lat = $stored_data['lat'] ?? ''; |
| | | $lng = $stored_data['lng'] ?? ''; |
| | | $street = $stored_data['street'] ?? ''; |
| | | |
| | | // 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 JavaScript configuration |
| | | $field_id = esc_attr($name); |
| | | $map_id = $field_id . '_map'; |
| | | // Prepare configuration for JavaScript initialization |
| | | $js_config = [ |
| | | 'fieldId' => $field_id, |
| | | 'initialCoords' => (!empty($lat) && !empty($lng)) ? [ |
| | |
| | | ] : null |
| | | ]; |
| | | |
| | | $this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use ( |
| | | $stored_data, $street, $address, $lat, $lng, $map_id, $js_config |
| | | ) { |
| | | ?> |
| | | <div class="location-field-wrapper" |
| | | data-location-field-init="<?= esc_attr(json_encode($js_config)) ?>"> |
| | | // IMPORTANT: Properly escape the JSON for use in HTML attribute |
| | | $json_config = htmlspecialchars(json_encode($js_config), ENT_QUOTES, 'UTF-8'); |
| | | ?> |
| | | |
| | | <?php if (!empty($street)) : ?> |
| | | <p class="current-location"> |
| | | <strong>Current location:</strong> <?= esc_html($street) ?> |
| | | </p> |
| | | <?php endif; ?> |
| | | <div class="field location <?= esc_attr($field_id) ?>" |
| | | data-field="<?= esc_attr($field_id) ?>" |
| | | data-location-field-init="<?= $json_config ?>"<?=$describedBy?>> |
| | | |
| | | <label for="<?= esc_attr($data['id']) ?>">Address</label> |
| | | <input type="text" |
| | | id="<?= esc_attr($data['id']) ?>" |
| | | name="<?= esc_attr($data['name']) ?>[address]" |
| | | value="<?= esc_attr($address) ?>" |
| | | placeholder="Enter an address" |
| | | class="location-search-input" |
| | | autocomplete="off" |
| | | <?= !empty($field['required']) ? 'required' : '' ?>> |
| | | |
| | | <div id="<?= esc_attr($map_id) ?>" class="location-map" style="height: 300px;"></div> |
| | | |
| | | <!-- Hidden fields for lat/lng --> |
| | | <input type="hidden" name="<?= esc_attr($data['name']) ?>[lat]" value="<?= esc_attr($lat) ?>" class="location-lat"> |
| | | <input type="hidden" name="<?= esc_attr($data['name']) ?>[lng]" value="<?= esc_attr($lng) ?>" class="location-lng"> |
| | | <input type="hidden" name="<?= esc_attr($data['name']) ?>[street]" value="<?= esc_attr($stored_data['street'] ?? '') ?>" class="location-street"> |
| | | <input type="hidden" name="<?= esc_attr($data['name']) ?>[city]" value="<?= esc_attr($stored_data['city'] ?? '') ?>" class="location-city"> |
| | | <input type="hidden" name="<?= esc_attr($data['name']) ?>[province]" value="<?= esc_attr($stored_data['province'] ?? '') ?>" class="location-province"> |
| | | <input type="hidden" name="<?= esc_attr($data['name']) ?>[postal]" value="<?= esc_attr($stored_data['postal'] ?? '') ?>" class="location-postal"> |
| | | <input type="hidden" name="<?= esc_attr($data['name']) ?>[country]" value="<?= esc_attr($stored_data['country'] ?? '') ?>" class="location-country"> |
| | | </div> |
| | | <?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 |
| | | } |
| | | |
| | | /* ========== HTML FIELD ========== */ |
| | |
| | | ); |
| | | } |
| | | |
| | | protected function renderHint(string $hint): void |
| | | { |
| | | ?> |
| | | <span class="hint"><?= esc_html($hint) ?></span> |
| | | <?php |
| | | } |
| | | |
| | | protected function renderDescription(string $description, string $name): void |
| | | { |
| | | ?> |
| | | <p class="description" id="<?= esc_attr($name) ?>-help"> |
| | | <?= wp_kses_post($description) ?> |
| | | </p> |
| | | <?php |
| | | } |
| | | |
| | | protected function getAllowedTypes(array $config): array |
| | | { |
| | | if (!empty($config['accepted_types'])) { |
| | |
| | | |
| | | return is_numeric($value) ? [$value] : []; |
| | | } |
| | | /** |
| | | * Render tag list field - inline tag input interface |
| | | */ |
| | | protected function renderTagListField(string $name, mixed $value, array $field): void |
| | | { |
| | | $values = is_array($value) ? $value : []; |
| | | $conditional = $this->handleConditionalField($field); |
| | | $validationAttrs = $this->buildValidationAttributes($field); |
| | | |
| | | if (array_key_exists('group', $field)) { |
| | | $name = $field['group'] . '::' . $name; |
| | | } |
| | | |
| | | $describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : ''; |
| | | |
| | | // Tag display format - defaults to first field value |
| | | $tagFormat = $field['tag_format'] ?? 'first_field'; |
| | | ?> |
| | | <div class="field tag-list <?= esc_attr($name) ?>" |
| | | data-field="<?= esc_attr($name) ?>" |
| | | data-tag-format="<?= esc_attr($tagFormat) ?>" |
| | | <?= $describedBy ?> |
| | | <?= $conditional ?> |
| | | <?= $validationAttrs ?>> |
| | | |
| | | <?php if (!empty($field['label'])): ?> |
| | | <h3><?= esc_html($field['label']) ?></h3> |
| | | <?php endif; ?> |
| | | |
| | | <!-- Inline input row --> |
| | | <div class="tag-input-row"> |
| | | <?php foreach ($field['fields'] as $subfield_name => $subfield_config): ?> |
| | | <?php |
| | | $subfield_config['label'] = $subfield_config['label'] ?? ucfirst($subfield_name); |
| | | $input_name = 'new_' . $subfield_name; |
| | | |
| | | // Store required state but don't render it on the input |
| | | // This prevents form submission validation but allows JS validation |
| | | |
| | | if (array_key_exists('required', $subfield_config)) { |
| | | $subfield_config['data']['required'] = true; |
| | | unset($subfield_config['required']); // Remove required for HTML rendering |
| | | } |
| | | $subfield_config['data']['ignore'] = true; |
| | | |
| | | $this->render($input_name, '', $subfield_config, false, false); |
| | | ?> |
| | | <?php endforeach; ?> |
| | | |
| | | <button type="button" class="button add-tag-item"> |
| | | <?= jvbIcon('plus') ?> <?= $field['add_label'] ?? 'Add' ?> |
| | | </button> |
| | | </div> |
| | | |
| | | <!-- Tags display --> |
| | | <div class="tag-items"> |
| | | <?php foreach ($values as $index => $item_data): ?> |
| | | <?php $this->renderTagItem($field['fields'], $item_data, $index, $name, $tagFormat); ?> |
| | | <?php endforeach; ?> |
| | | </div> |
| | | |
| | | <!-- Template for new tags --> |
| | | <template class="tag-template"> |
| | | <?php $this->renderTagItem($field['fields'], [], '', $name, $tagFormat); ?> |
| | | </template> |
| | | |
| | | <?php if (!empty($field['hint'])): ?> |
| | | <?php $this->renderHint($field['hint']); ?> |
| | | <?php endif; ?> |
| | | |
| | | <?php if (!empty($field['description'])): ?> |
| | | <?php $this->renderDescription($field['description'], $name); ?> |
| | | <?php endif; ?> |
| | | </div> |
| | | <?php |
| | | } |
| | | |
| | | /** |
| | | * Render individual tag item |
| | | */ |
| | | protected function renderTagItem(array $fields, array $data, int|string $index, string $base_name, string $format): void |
| | | { |
| | | $tag_text = $this->getTagDisplayText($fields, $data, $format); |
| | | ?> |
| | | <div class="tag-item" data-index="<?= esc_attr($index) ?>"> |
| | | <span class="tag-label"><?= esc_html($tag_text) ?></span> |
| | | |
| | | <!-- Hidden inputs for data --> |
| | | <?php foreach ($fields as $field_name => $field_config): ?> |
| | | <?php |
| | | $value = $data[$field_name] ?? ''; |
| | | $full_name = is_string($index) ? $field_name : "{$base_name}:{$index}:{$field_name}"; |
| | | ?> |
| | | <input type="hidden" |
| | | name="<?= esc_attr($full_name) ?>" |
| | | value="<?= esc_attr($value) ?>" |
| | | data-field="<?= esc_attr($field_name) ?>" /> |
| | | <?php endforeach; ?> |
| | | |
| | | <button type="button" class="remove-tag" aria-label="Remove"> |
| | | <?= jvbIcon('x') ?> |
| | | </button> |
| | | </div> |
| | | <?php |
| | | } |
| | | |
| | | /** |
| | | * Get tag display text based on format |
| | | */ |
| | | protected function getTagDisplayText(array $fields, array $data, string $format): string |
| | | { |
| | | if (empty($data)) { |
| | | return 'New Item'; |
| | | } |
| | | |
| | | switch ($format) { |
| | | case 'first_field': |
| | | // Use the first field's value |
| | | $first_key = array_key_first($fields); |
| | | return $data[$first_key] ?? 'New Item'; |
| | | |
| | | case 'all_fields': |
| | | // Show all field values separated by commas |
| | | $values = array_filter(array_values($data)); |
| | | return implode(', ', $values) ?: 'New Item'; |
| | | |
| | | case 'custom': |
| | | // Custom format - would need callback |
| | | return 'New Item'; |
| | | |
| | | default: |
| | | // Format is a template string like "{name} ({email})" |
| | | if (strpos($format, '{') !== false) { |
| | | $text = $format; |
| | | foreach ($data as $key => $value) { |
| | | $text = str_replace('{' . $key . '}', $value, $text); |
| | | } |
| | | return $text; |
| | | } |
| | | // Use specific field name |
| | | return $data[$format] ?? 'New Item'; |
| | | } |
| | | } |
| | | } |