From b0194e10a87e16797a568d8a30d53ebecd27d8a4 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 18 Oct 2025 15:04:51 +0000
Subject: [PATCH] =DataStore.js and UploaderManager.js overhaul
---
inc/meta/MetaForm.php | 396 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 392 insertions(+), 4 deletions(-)
diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index 7e7e66d..d6846c4 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -1298,6 +1298,396 @@
<?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;
@@ -1327,8 +1717,6 @@
<?=$dataContent?>
<?= ' data-type="'.$dataType.'"'?>>
-
-
<div class="file-upload-container">
<div class="file-upload-wrapper">
<input type="file"
@@ -1401,7 +1789,7 @@
</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">
@@ -1418,7 +1806,7 @@
<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; ?>
--
Gitblit v1.10.0