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') ?>&emsp;These will become individual <?= $plural ?>&emsp;<?= 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') ?>&emsp;Each group will become its own <?= $singular ?>&emsp;<?= 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