Jake Vanderwerf
2026-01-11 181938ea32cf2c11d10d56018df22586adff7860
inc/meta/MetaForm.php
@@ -202,6 +202,12 @@
      $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 ?>
@@ -217,6 +223,7 @@
               name="<?= esc_attr($data['name']) ?>"
               value="<?= esc_attr($data['value']) ?>"
               <?= $inputAttrs ?>
               <?= $customData?>
            >
            <span class="validation-icon success" hidden aria-hidden="true">
                    <?= jvbIcon('check-circle') ?>
@@ -475,8 +482,7 @@
            <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) ?>
@@ -812,7 +818,7 @@
         //Processing Options
         'max_size' => null, // Override default size limits
         'convert' => 'webp', // Image conversion format
         'quality' => 80, // Conversion quality
         'quality' => 90, // Conversion quality
         'create_thumbnails' => true,
      ];
      $config = array_merge($defaultConfig, $field);
@@ -911,6 +917,7 @@
               <?php endif; ?>
               <div class="file-error"></div>
            </div>
            <?php jvbRenderProgressBar(); ?>
         </div>
@@ -921,7 +928,7 @@
                  <div class="selection-controls">
                     <div class="selected">
                        <div class="field">
                           <input type="checkbox" id="select-all-uploads" name="select-all-uploads">
                           <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>
@@ -988,6 +995,29 @@
      <?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
    */
@@ -1070,71 +1100,72 @@
      $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 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>
            <details>
               <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
         </div>
         <details>
            <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
      <?php
            <?php
            $fields = array_key_exists('fields', $config) ? $config['fields'] : [];
      $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);
            // Only add image_data if not already provided
            if (!array_key_exists('image_data', $fields)) {
               $fields['image_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
                     ]
                  ]
               ];
            }
      $meta = new MetaManager($id);
      foreach ($fields as $field => $config) {
         $meta->render('form', $field, $config);
      }
      ?>
            </details>
            $meta = new MetaManager($id);
            foreach ($fields as $field => $config) {
               $meta->render('form', $field, $config);
            }
            ?>
         </details>
      </div>
      <?php
   }
@@ -1321,13 +1352,12 @@
    * 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"' : '';
      $isSimple = (array_key_exists('mode', $field) && $field['mode']==='simple');
      // Parse selected values
      $value = (is_array($value)) ? array_filter(array_map('absint', $value)): $value;
      $selected = ($value === '') ? [] : (is_array($value) ? $value : explode(',', $value));
@@ -1378,9 +1408,10 @@
      }
      ?>
      <div class="field <?= esc_attr($type) ?> <?= 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 ?>>
@@ -1588,5 +1619,147 @@
      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';
      }
   }
}