| | |
| | | <?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; |
| | |
| | | <?=$dataContent?> |
| | | <?= ' data-type="'.$dataType.'"'?>> |
| | | |
| | | |
| | | |
| | | <div class="file-upload-container"> |
| | | <div class="file-upload-wrapper"> |
| | | <input type="file" |
| | |
| | | </div> |
| | | |
| | | <?php if ($groupable) : ?> |
| | | <p class="hint"><?= jvbIcon('elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('elbow-right-up')?></p> |
| | | <p class="hint"><?= jvbIcon('elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('elbow-right-up')?></p> |
| | | </div> |
| | | <div class="sidebar"> |
| | | <div class="header"> |
| | |
| | | <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> |
| | | <p class="hint"><?= jvbIcon('elbow-left-up') ?> Each group will become its own <?= $singular ?> <?= jvbIcon('elbow-right-up')?></p> |
| | | </div> |
| | | </div> |
| | | <?php endif; ?> |