From a81f7043fc44382775f9afac48e4c7a651e7ac6c Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 04 Jan 2026 18:29:10 +0000
Subject: [PATCH] =PopulateForm.js and ContentRoutes.php minor changes

---
 assets/js/concise/UploadManagerOld.js | 3141 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 3,141 insertions(+), 0 deletions(-)

diff --git a/assets/js/concise/UploadManagerOld.js b/assets/js/concise/UploadManagerOld.js
new file mode 100644
index 0000000..3f1ae58
--- /dev/null
+++ b/assets/js/concise/UploadManagerOld.js
@@ -0,0 +1,3141 @@
+/**
+ * UploadManager - Refactored for clarity
+ *
+ * Architecture:
+ * - DataStores (fieldStore, uploadStore) = Recovery cache only, cleared after successful upload
+ * - Maps (uploadElements, fieldElements) = Runtime DOM references
+ * - Upload data flows: File → Process → Queue → Server → Clean up stores
+ */
+class UploadManager {
+	constructor() {
+		// Load dependencies
+		this.queue = window.jvbQueue;
+		this.a11y = window.jvbA11y;
+		this.error = window.jvbError;
+		this.fieldStoreReady = false;
+		this.uploadStoreReady = false;
+		this.hasCheckedForUploads = false;
+		const {fields, uploads} = window.jvbStore.register(
+			'uploads',
+			[
+				{
+					storeName: 'fields',
+					keyPath: 'id',
+					indexes: [
+						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'timestamp', keyPath: 'timestamp' },
+						{ name: 'content', keyPath: 'content' },
+						{ name: 'itemId', keyPath: 'itemId' },
+						{ name: 'status', keyPath: 'status' }
+					],
+					TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
+					delayFetch: true
+				},
+				{
+					storeName: 'uploads',
+					keyPath: 'id',
+					storeBlobs: true,
+					indexes: [
+						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'status', keyPath: 'status' },
+						{ name: 'groupId', keyPath: 'groupId' },
+						{ name: 'attachmentId', keyPath: 'attachmentId' }
+					],
+					delayFetch: true
+				}
+			]
+		);
+		this.fieldStore = fields;
+		this.uploadStore = uploads;
+
+		window.jvbUploadBlobs = this.uploadStore;
+
+		// Subscribe to store events
+		this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
+		this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
+
+		// RUNTIME DATA - DOM references and ephemeral state
+		this.uploadElements = new Map();  // uploadId → { element, preview, location }
+		this.fieldElements = new Map();   // fieldId → { element, ui, config }
+		this.groupElements = new Map();   // groupId → { element, grid, fieldId }
+
+		// Selection and UI state
+		this.selected = new Map();
+		this.selectionHandlers = new Map();
+		this.previewUrls = new Set();
+		this.sortableInstances = new Map();
+
+		// Worker for image processing
+		this.initWorker();
+
+		// Notification subscribers
+		this.subscribers = new Set();
+
+		// Selectors
+		this.selectors = {
+			field: {
+				field: '[data-upload-field]',
+				input: 'input[type="file"]',
+				dropZone: '.file-upload-container',
+				preview: '.item-grid.preview',
+				progress: '.image-progress'
+			},
+			groups: {
+				container: '.upload-group',
+				grid: '.item-grid.group',
+				header: '.group-header',
+				selectAll: '[name="select-all-group"]',
+				actions: '.group-actions',
+				count: '.selection-controls .info'
+			},
+			items: {
+				item: '[data-upload-id]',
+				checkbox: '[name*="select-item"]',
+				featured: '[name="featured"]',
+				details: 'details'
+			}
+		};
+
+		this.statusMapping = {
+			'received': 'Image Received',
+			'local_processing': 'Processing Image...',
+			'queued': 'Waiting to upload...',
+			'uploading': 'Uploading to Server',
+			'pending': 'Successfully sent to server. In line for further processing.',
+			'processing': 'Processing on server...',
+			'completed': 'Upload complete!',
+			'failed': 'Upload failed (will retry)',
+			'failed_permanent': 'Upload failed permanently'
+		};
+
+		this.init();
+	}
+
+	async init() {
+		// this.initializeFields();
+		this.initListeners();
+
+		// Queue integration - handle completion/failure
+		this.queue.subscribe((event, operation) => {
+			if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
+				return;
+			}
+
+			const fieldId = operation.data instanceof FormData
+				? operation.data.get('fieldId')
+				: operation.data?.fieldId;
+
+			switch(event) {
+				case 'cancel-operation':
+					if (fieldId) this.handleOperationCancelled(fieldId);
+					break;
+				case 'operation-status':
+					if (fieldId) this.updateFieldStatus(fieldId, operation.status);
+					break;
+				case 'operation-complete':
+					this.handleOperationComplete(operation, fieldId);
+					break;
+				case 'operation-failed':
+				case 'operation-failed-permanent':
+					this.handleOperationFailed(operation, fieldId);
+					break;
+			}
+		});
+
+		window.addEventListener('beforeunload', () => {
+			this.cleanupAllPreviewUrls();
+		});
+	}
+
+	initWorker() {
+		this.worker = {
+			worker: null,
+			timeout: null,
+			tasks: new Map(),
+			restart: { count: 0, max: 3 },
+			settings: {
+				timeout: 10000,
+				batchSize: 1,
+				maxConcurrent: 3,
+				restartAfterTimeout: true
+			}
+		};
+	}
+
+	/*******************************************************************************
+	 * FIELD MANAGEMENT
+	 *******************************************************************************/
+	scanFields(container, autoUpload) {
+		console.log(autoUpload, 'autoUpload');
+		const fields = container.querySelectorAll(this.selectors.field.field);
+		fields.forEach(uploader => this.registerUploader(uploader, autoUpload));
+	}
+
+	registerUploader(uploader, autoUpload) {
+		const fieldId = this.determineFieldId(uploader);
+		const config = this.extractFieldConfig(uploader, autoUpload);
+		const ui = this.buildFieldUI(uploader);
+
+		console.log(config, 'registering with config');
+		// Store field data with Sets for runtime
+		const fieldData = {
+			id: fieldId,
+			config: config,
+			uploads: new Set(),
+			groups: [],
+			state: 'ready',
+			timestamp: Date.now()
+		};
+
+		// Save to store (will convert Sets to Arrays automatically)
+		this.fieldStore.save(fieldData);
+
+		// Store DOM references separately
+		this.fieldElements.set(fieldId, { element: uploader, ui, config });
+
+		uploader.dataset.uploader = fieldId;
+		this.addFieldSelectionHandler(fieldId);
+
+		if (config.type !== 'single') {
+			this.initSortable(fieldId);
+		}
+
+		return fieldId;
+	}
+
+	extractFieldConfig(fieldElement, autoUpload) {
+		return {
+			autoUpload: autoUpload,
+			destination: fieldElement.dataset.destination || 'meta',
+			content: fieldElement.dataset.content || null,
+			mode: fieldElement.dataset.mode || 'direct',
+			type: fieldElement.dataset.type || 'single',
+			name: fieldElement.dataset.field,
+			itemID: fieldElement.dataset.itemId || 0,
+			maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
+			subtype: fieldElement.dataset.subtype || 'image'
+		};
+	}
+
+	buildFieldUI(fieldElement) {
+		let UI = {
+			field: fieldElement,
+			input: fieldElement.querySelector(this.selectors.field.input),
+			dropZone: fieldElement.querySelector(this.selectors.field.dropZone),
+			preview: fieldElement.querySelector(this.selectors.field.preview),
+			progress: {
+				progress: fieldElement.querySelector(this.selectors.field.progress),
+				bar: fieldElement.querySelector('.bar'),
+				fill: fieldElement.querySelector('.fill'),
+				details: fieldElement.querySelector('.details'),
+				text: fieldElement.querySelector('.details .text'),
+				count: fieldElement.querySelector('.details .count')
+			}
+		};
+
+		let display = fieldElement.querySelector('.group-display');
+		if (display) {
+			UI.groups = {
+				display: display,
+				container: fieldElement.querySelector('.item-grid.groups'),
+				empty: fieldElement.querySelector('.empty-group'),
+				groups: new Map()
+			};
+		}
+
+		return UI;
+	}
+
+	/*******************************************************************************
+	 * SORTABLE INITIALIZATION
+	 *******************************************************************************/
+	initSortable(fieldId) {
+		if (!window.Sortable) return;
+
+		// Mount MultiDrag plugin once
+		if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
+			Sortable.mount(new Sortable.MultiDrag());
+			Sortable._multiDragMounted = true;
+		}
+
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		// Initialize sortable on all existing grids
+		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+		grids.forEach(grid => {
+			const groupId = grid.classList.contains('group')
+				? grid.closest('.upload-group')?.dataset.groupId
+				: null;
+			this.createSortableForGrid(grid, fieldId, groupId);
+		});
+
+		// Special handler for empty-group
+		const emptyGroup = fieldEl.element.querySelector('.empty-group');
+		if (emptyGroup && !emptyGroup.sortableInstance) {
+			emptyGroup.sortableInstance = new Sortable(emptyGroup, {
+				animation: 150,
+				draggable: '.item',
+				multiDrag: true,
+				selectedClass: 'selected-for-drag',
+				avoidImplicitDeselect: true,
+				group: { name: fieldId, pull: false, put: true },
+				ghostClass: 'sortable-ghost',
+				chosenClass: 'sortable-chosen',
+				dragClass: 'sortable-drag',
+				onEnd: (evt) => this.handleDrop(evt, fieldId)
+			});
+		}
+	}
+
+	syncSortableSelection(fieldId, selectedItems) {
+		// Update Sortable's selection state to match checkboxes
+		this.sortableInstances.forEach((instance, key) => {
+			if (key.startsWith(fieldId)) {
+				const grid = instance.el;
+				const items = grid.querySelectorAll('.item');
+
+				items.forEach(item => {
+					const uploadId = item.dataset.uploadId;
+					const shouldBeSelected = selectedItems.has(uploadId);
+
+					if (shouldBeSelected) {
+						Sortable.utils.select(item);
+					} else {
+						Sortable.utils.deselect(item);
+					}
+				});
+			}
+		});
+	}
+
+	handleDrop(evt, fieldId) {
+		const dropTarget = evt.to;
+		const sourceTarget = evt.from;
+		const items = evt.items?.length > 0 ? evt.items : [evt.item];
+		const uploadIds = items.map(item => item.dataset.uploadId);
+
+		// Determine drop target type
+		const targetType = this.getDropTargetType(dropTarget);
+
+		switch (targetType) {
+			case 'empty-group':
+				this.handleDropToEmptyGroup(items, uploadIds, fieldId);
+				break;
+
+			case 'preview':
+				this.handleDropToPreview(items, uploadIds, fieldId);
+				break;
+
+			case 'group':
+				this.handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId);
+				break;
+			default:
+				// Fallback: return to preview
+				this.handleDropToPreview(items, uploadIds, fieldId);
+				break;
+		}
+
+		// Update UI
+		this.updateSortableState(dropTarget);
+		if (sourceTarget !== dropTarget) {
+			this.updateSortableState(sourceTarget);
+		}
+	}
+
+	/**
+	 * Determine what type of drop target this is
+	 */
+	getDropTargetType(target) {
+		if (target.classList.contains('empty-group')) {
+			return 'empty-group';
+		}
+
+		if (target.classList.contains('preview')) {
+			return 'preview';
+		}
+
+		if (target.classList.contains('group')) {
+			return 'group';
+		}
+
+		return 'unknown';
+	}
+
+	/**
+	 * Handle drop to group: add to existing group
+	 */
+	handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId) {
+		try {
+			// If same container, it's just a reorder
+			if (dropTarget === sourceTarget) {
+				this.handleReorder({ to: dropTarget, items: items });
+				return;
+			}
+
+			// Moving to different group
+			uploadIds.forEach(uploadId => {
+				this.addToGroup(uploadId, dropTarget, false);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Moved ${items.length} items to group`
+				: 'Moved item to group';
+			this.a11y.announce(message);
+
+			// Clear selection
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		}  catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * Handle drop to preview: remove from groups
+	 */
+	handleDropToPreview(items, uploadIds, fieldId) {
+		try {
+			uploadIds.forEach(uploadId => {
+				this.removeFromGroup(uploadId);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Moved ${items.length} items to preview`
+				: 'Moved item to preview';
+			this.a11y.announce(message);
+
+			// Clear selection
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		} catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * Handle drop errors consistently
+	 */
+	handleDropError(items, fieldId, error, message = 'An error occurred') {
+		console.error('Drop error:', error);
+
+		// Return items to preview as fallback
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (fieldEl?.ui?.preview) {
+			items.forEach(item => fieldEl.ui.preview.appendChild(item));
+		}
+
+		this.a11y.announce(`${message}. Items returned to preview.`);
+	}
+
+	/**
+	 * Handle drop to group: add to existing group
+	 */
+	handleDropToEmptyGroup(items, uploadIds, fieldId) {
+		try {
+			const group = this.createGroup(fieldId);
+			if (!group) {
+				this.handleDropError(items, fieldId, new Error('Group creation failed'), 'Failed to create group');
+				return;
+			}
+
+			// Move items to new group
+			items.forEach((item, index) => {
+				group.grid.appendChild(item);
+				this.addToGroup(uploadIds[index], group.grid, false);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Created group with ${items.length} items`
+				: 'Created group with item';
+			this.a11y.announce(message);
+
+			// Clear selection after move
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		} catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * Update sortable enabled/disabled state based on item count
+	 */
+	updateSortableState(grid) {
+		const sortable = grid?.sortableInstance;
+		if (!sortable) return;
+
+		// const hasItems = grid.querySelectorAll('.item').length > 0;
+		sortable.option('disabled', false);
+	}
+
+	/**
+	 * Refresh sortable for a field (call after adding/removing items dynamically)
+	 */
+	refreshSortable(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+		grids.forEach(grid => this.updateSortableState(grid));
+	}
+
+	handleReorder(evt) {
+		const grid = evt.to;
+		const fieldWrapper = grid.closest('.field, .upload');
+		if (!fieldWrapper) return;
+
+		// Get current order from DOM
+		let items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
+			.map(upload => upload.dataset.uploadId)
+			.filter(id => id);
+
+
+		// Update hidden input (for form submission)
+		let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
+		if (hiddenInput && items.length > 0) {
+			hiddenInput.value = items.join(',');
+		}
+
+		// Update fieldState with new order
+		const fieldId = this.getFieldIdFromElement(grid);
+		if (fieldId) {
+			const fieldData = this.getFieldData(fieldId);
+
+			// If reordering within a group, update that group's uploads array
+			if (grid.classList.contains('group')) {
+				const groupId = grid.dataset.groupId;
+				const group = fieldData?.groups?.find(g => g.id === groupId);
+				if (group) {
+					group.uploads = items; // Update order
+				}
+			}
+			// If reordering in preview, the order is implicit by DOM position
+			// (we don't store preview order separately)
+
+			this.schedulePersistance(fieldId);
+		}
+
+		this.a11y.announce('Item reordered');
+
+		fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
+			detail: {
+				from: evt.from,
+				to: evt.to,
+				oldIndex: evt.oldIndex,
+				newIndex: evt.newIndex,
+				items: items
+			},
+			bubbles: true
+		}));
+	}
+
+	/*******************************************************************************
+	 * FILE DROP HANDLERS
+	 *******************************************************************************/
+
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+
+		this.dragEnterHandler = this.handleExternalDragEnter.bind(this);
+		this.dragLeaveHandler = this.handleExternalDragLeave.bind(this);
+		this.dragOverHandler = this.handleExternalDragOver.bind(this);
+		this.dropHandler = this.handleExternalDrop.bind(this);
+
+		document.addEventListener('dragenter', this.dragEnterHandler);
+		document.addEventListener('dragleave', this.dragLeaveHandler);
+		document.addEventListener('dragover', this.dragOverHandler);
+		document.addEventListener('drop', this.dropHandler);
+	}
+
+	handleExternalDragLeave(e) {
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone && !dropZone.contains(e.relatedTarget)) {
+			dropZone.classList.remove('dragover');
+		}
+	}
+
+	handleExternalDragEnter(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			dropZone.classList.add('dragover');
+		}
+	}
+
+	handleExternalDragOver(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			e.dataTransfer.dropEffect = 'copy';
+		}
+	}
+
+	handleExternalDrop(e) {
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (!dropZone) return;
+
+		e.preventDefault();
+		dropZone.classList.remove('dragover');
+
+		const files = Array.from(e.dataTransfer.files);
+		if (files.length === 0) return;
+
+		const fieldId = this.getFieldIdFromElement(dropZone);
+		if (fieldId) {
+			this.processFiles(fieldId, files);
+			this.a11y.announce(`${files.length} file(s) dropped for upload`);
+		}
+	}
+
+	/*******************************************************************************
+	 * CLICK & CHANGE HANDLERS
+	 *******************************************************************************/
+
+	handleClick(e) {
+		// Trigger file input
+		if (e.target.matches(this.selectors.field.dropZone) ||
+			e.target.closest(this.selectors.field.dropZone)) {
+			const dropZone = e.target.closest(this.selectors.field.dropZone);
+			if (dropZone && !e.target.matches('input, button, a')) {
+				const input = dropZone.querySelector(this.selectors.field.input);
+				input?.click();
+			}
+		}
+
+		// Group actions
+		const actionButton = e.target.closest('[data-action]');
+		if (actionButton) {
+			this.handleAction(actionButton);
+		}
+	}
+
+	handleChange(e) {
+		const fieldId = this.getFieldIdFromElement(e.target);
+
+		// File input change
+		if (e.target.matches(this.selectors.field.input)) {
+			const files = Array.from(e.target.files);
+			if (files.length > 0 && fieldId) {
+				this.processFiles(fieldId, files);
+			}
+		}
+
+		// Meta field changes
+		if (fieldId) {
+			const fieldData = this.getFieldData(fieldId);
+			if (!fieldData.config.autoUpload) {
+				return;
+			}
+			if (fieldData?.config.destination === 'post_group') {
+				this.handleGroupMetaChange(e.target);
+			} else {
+				this.queueUploadMeta(e);
+			}
+		}
+	}
+
+	/*******************************************************************************
+	 * FILE PROCESSING
+	 *******************************************************************************/
+
+	async processFiles(fieldId, files) {
+		const fieldData = this.getFieldData(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldData || !fieldEl) return;
+
+		// Show group display, hide upload zone
+		if (fieldEl.ui.dropZone) {
+			fieldEl.ui.dropZone.hidden = true;
+		}
+		if (fieldEl.ui.groups?.display) {
+			fieldEl.ui.groups.display.hidden = false;
+		}
+
+		const totalFiles = files.length;
+		let processedCount = 0;
+
+		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+		const processPromises = Array.from(files).map(async (file) => {
+			try {
+				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+				// Create initial upload data
+				const uploadData = {
+					id: uploadId,
+					attachmentId: null,
+					fieldId: fieldId,
+					status: 'local_processing',
+					groupId: null,
+					meta: {
+						originalName: file.name,
+						size: file.size,
+						type: file.type
+					}
+				};
+
+				// Save initial data
+				await this.uploadStore.save(uploadData);
+
+				// Process file
+				const preview = this.createPreviewUrl(file);
+				const processedFile = file.type.startsWith('image/')
+					? await this.processImage(file, fieldData.config.subtype)
+					: file;
+
+				// Show progress
+				this.showUploadProgress(uploadId, true);
+				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
+
+				// Store blob data (this updates the existing uploadData)
+				await this.saveBlobData(uploadId, processedFile || file);
+
+				// Create DOM element
+				const subtype = this.getSubtypeFromMime(file.type);
+				const element = this.createUploadElement({
+					id: uploadId,
+					preview: preview,
+					meta: uploadData.meta,
+					subtype: subtype
+				}, fieldData.config.destination === 'post_group');
+
+				// Add to preview grid
+				if (fieldEl.ui.preview) {
+					fieldEl.ui.preview.appendChild(element);
+
+					// Store runtime element data
+					this.uploadElements.set(uploadId, {
+						element: element,
+						preview: preview,
+						location: fieldEl.ui.preview
+					});
+				}
+
+				// Update status (gets existing data with blobData intact)
+				const storedUpload = this.uploadStore.get(uploadId);
+				if (storedUpload) {
+					storedUpload.status = 'processed';
+					await this.uploadStore.save(storedUpload);
+				}
+
+				// Add to field
+				fieldData.uploads.add(uploadId);
+				await this.saveFieldData(fieldData);
+
+				// Update progress
+				processedCount++;
+				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				this.updateUploadItemProgress(uploadId, 100, 'processed');
+
+				// Fade out progress
+				setTimeout(() => this.showUploadProgress(uploadId, false), 1000);
+
+				return uploadId;
+
+			} catch (error) {
+				console.error('Error processing file:', file.name, error);
+				processedCount++;
+				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				return null;
+			}
+		});
+
+		await Promise.all(processPromises);
+
+		this.updateFieldState(fieldId);
+		this.refreshSortable(fieldId);
+
+		// Queue for upload if in direct mode
+		if (fieldData.config.autoUpload && fieldData.config.destination !== 'post_group') {
+			await this.queueUpload(fieldId);
+			this.maybeLockUploads(fieldId);
+		}
+	}
+
+	/*******************************************************************************
+	 * IMAGE PROCESSING
+	 *******************************************************************************/
+
+	async processImage(file, uploadId) {
+		const timeout = this.worker.settings.timeout;
+
+		return new Promise((resolve, reject) => {
+			let timeoutId;
+			let taskCompleted = false;
+
+			timeoutId = setTimeout(() => {
+				if (!taskCompleted) {
+					taskCompleted = true;
+					this.worker.tasks.delete(uploadId);
+					if (this.worker.settings.restartAfterTimeout) {
+						this.restartCompressionWorker();
+					}
+					reject(new Error(`Processing timeout for ${file.name}`));
+				}
+			}, timeout);
+
+			this.worker.tasks.set(uploadId, { file, timeoutId });
+
+			this.handleProcess(file, uploadId)
+				.then(result => {
+					if (!taskCompleted) {
+						taskCompleted = true;
+						clearTimeout(timeoutId);
+						this.worker.tasks.delete(uploadId);
+						resolve(result);
+					}
+				})
+				.catch(error => {
+					if (!taskCompleted) {
+						taskCompleted = true;
+						clearTimeout(timeoutId);
+						this.worker.tasks.delete(uploadId);
+						reject(error);
+					}
+				});
+		});
+	}
+
+	async handleProcess(file, uploadId) {
+		if (!file.type.startsWith('image/')) {
+			return file;
+		}
+
+		const maxDimension = this.getMaxDimension();
+		const quality = 0.85;
+
+		if (this.shouldUseWorker(file)) {
+			try {
+				if (!this.worker.worker) {
+					this.initCompressionWorker();
+				}
+				if (this.worker.worker) {
+					return await this.processWithWorker(file, uploadId, maxDimension, quality);
+				}
+			} catch (error) {
+				console.warn('Worker processing failed, falling back to main thread:', error);
+			}
+		}
+
+		return await this.processOnMainThread(file, maxDimension, quality);
+	}
+
+	async processOnMainThread(file, maxDimension, quality) {
+		return new Promise((resolve, reject) => {
+			const img = new Image();
+			const canvas = document.createElement('canvas');
+			const ctx = canvas.getContext('2d');
+			let objectUrl = null;
+
+			const cleanup = () => {
+				img.onload = null;
+				img.onerror = null;
+				if (objectUrl) {
+					URL.revokeObjectURL(objectUrl);
+					objectUrl = null;
+				}
+				canvas.width = 1;
+				canvas.height = 1;
+				ctx.clearRect(0, 0, 1, 1);
+			};
+
+			img.onload = () => {
+				try {
+					const { width, height } = this.calculateOptimalDimensions(img, maxDimension);
+					canvas.width = width;
+					canvas.height = height;
+
+					ctx.imageSmoothingEnabled = true;
+					ctx.imageSmoothingQuality = 'high';
+					ctx.drawImage(img, 0, 0, width, height);
+
+					const outputFormat = this.getOptimalFormat(file);
+					const outputQuality = this.getOptimalQuality(file, quality);
+
+					canvas.toBlob(
+						(blob) => {
+							cleanup();
+							if (blob) {
+								const processedFile = new File(
+									[blob],
+									this.getProcessedFileName(file, outputFormat),
+									{ type: outputFormat, lastModified: Date.now() }
+								);
+								resolve(processedFile);
+							} else {
+								reject(new Error('Canvas toBlob failed'));
+							}
+						},
+						outputFormat,
+						outputQuality
+					);
+
+				} catch (error) {
+					cleanup();
+					reject(new Error(`Canvas processing failed: ${error.message}`));
+				}
+			};
+
+			img.onerror = () => {
+				cleanup();
+				reject(new Error(`Failed to load image: ${file.name}`));
+			};
+
+			try {
+				objectUrl = this.createPreviewUrl(file);
+				img.src = objectUrl;
+			} catch (error) {
+				cleanup();
+				reject(new Error(`Failed to create object URL: ${error.message}`));
+			}
+		});
+	}
+
+	getOptimalFormat(file) {
+		if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
+			return file.type;
+		}
+		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
+	}
+
+	getOptimalQuality(file, requestedQuality) {
+		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
+		if (file.size < 2 * 1024 * 1024) return requestedQuality;
+		return Math.min(requestedQuality, 0.8);
+	}
+
+	getProcessedFileName(originalFile, outputFormat) {
+		const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
+		const extensions = {
+			'image/webp': '.webp',
+			'image/jpeg': '.jpg',
+			'image/png': '.png',
+			'image/gif': '.gif'
+		};
+		return baseName + (extensions[outputFormat] || '.jpg');
+	}
+
+	getMaxDimension() {
+		const screenWidth = window.screen.width;
+		const devicePixelRatio = window.devicePixelRatio || 1;
+		if (screenWidth * devicePixelRatio > 2560) return 2400;
+		if (screenWidth * devicePixelRatio > 1920) return 1920;
+		return 1200;
+	}
+
+	shouldUseWorker(file) {
+		return this.worker.worker &&
+			file.size > 1024 * 1024 &&
+			typeof OffscreenCanvas !== 'undefined';
+	}
+
+	async processWithWorker(file, uploadId, maxDimension, quality) {
+		return new Promise((resolve, reject) => {
+			if (!this.worker.worker) {
+				reject(new Error('Worker not available'));
+				return;
+			}
+
+			const messageId = `${uploadId}_${Date.now()}`;
+
+			const messageHandler = (e) => {
+				if (e.data.messageId !== messageId) return;
+
+				this.worker.worker.removeEventListener('message', messageHandler);
+				this.worker.worker.removeEventListener('error', errorHandler);
+
+				if (e.data.success) {
+					const processedFile = new File(
+						[e.data.blob],
+						this.getProcessedFileName(file, e.data.format || 'image/webp'),
+						{ type: e.data.format || 'image/webp', lastModified: Date.now() }
+					);
+					resolve(processedFile);
+				} else {
+					reject(new Error(e.data.error || 'Worker processing failed'));
+				}
+			};
+
+			const errorHandler = (error) => {
+				this.worker.worker.removeEventListener('message', messageHandler);
+				this.worker.worker.removeEventListener('error', errorHandler);
+				reject(new Error(`Worker error: ${error.message}`));
+			};
+
+			this.worker.worker.addEventListener('message', messageHandler);
+			this.worker.worker.addEventListener('error', errorHandler);
+
+			this.worker.worker.postMessage({
+				messageId,
+				file,
+				maxDimension,
+				quality,
+				outputFormat: this.getOptimalFormat(file)
+			});
+		});
+	}
+
+	restartCompressionWorker() {
+		if (this.worker.worker) {
+			this.worker.worker.terminate();
+			this.worker.worker = null;
+		}
+		this.worker.tasks.clear();
+		if (this.worker.restart.count >= this.worker.restart.max) {
+			console.error('Max worker restarts reached, disabling worker');
+			return;
+		}
+		this.worker.restart.count++;
+		this.initCompressionWorker();
+	}
+
+	initCompressionWorker() {
+		if (this.worker.worker || typeof Worker === 'undefined') return;
+
+		try {
+			const workerScript = `
+				self.onmessage = async function(e) {
+					const { messageId, file, maxDimension, quality, outputFormat } = e.data;
+					try {
+						const bitmap = await createImageBitmap(file);
+						const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
+						const width = Math.round(bitmap.width * scale);
+						const height = Math.round(bitmap.height * scale);
+						const canvas = new OffscreenCanvas(width, height);
+						const ctx = canvas.getContext('2d');
+						ctx.imageSmoothingEnabled = true;
+						ctx.imageSmoothingQuality = 'high';
+						ctx.drawImage(bitmap, 0, 0, width, height);
+						bitmap.close();
+						const blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });
+						self.postMessage({ messageId, success: true, blob: blob, format: outputFormat });
+					} catch (error) {
+						self.postMessage({ messageId, success: false, error: error.message });
+					}
+				};
+			`;
+
+			const blob = new Blob([workerScript], { type: 'application/javascript' });
+			this.worker.worker = new Worker(this.createPreviewUrl(blob));
+
+		} catch (error) {
+			console.warn('Failed to initialize compression worker:', error);
+			this.worker.worker = null;
+		}
+	}
+
+	calculateOptimalDimensions(img, maxDimension) {
+		let { width, height } = img;
+		if (width <= maxDimension && height <= maxDimension) {
+			return { width, height };
+		}
+		const scale = Math.min(maxDimension / width, maxDimension / height);
+		return {
+			width: Math.round(width * scale),
+			height: Math.round(height * scale)
+		};
+	}
+
+	supportsWebP() {
+		const canvas = document.createElement('canvas');
+		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
+	}
+
+	createPreviewUrl(file) {
+		const url = URL.createObjectURL(file);
+		if (!this.previewUrls) this.previewUrls = new Set();
+		this.previewUrls.add(url);
+		return url;
+	}
+
+	revokePreviewUrl(url) {
+		if (url?.startsWith('blob:')) {
+			URL.revokeObjectURL(url);
+			this.previewUrls?.delete(url);
+		}
+	}
+
+	/*******************************************************************************
+	 * QUEUE INTEGRATION
+	 *******************************************************************************/
+
+	async submitUploads(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldData?.uploads || fieldData.uploads.size === 0) {
+			return;
+		}
+
+		let uploadIds = Array.from(fieldData.uploads);
+		if (uploadIds.length === 0) {
+			this.error.log('No uploads to upload', {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			return;
+		}
+
+		const fieldGroups = this.getFieldGroups(fieldId);
+
+		if (fieldGroups.length === 0) {
+			this.error.log('No groups created for post_group upload', {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			return;
+		}
+
+		// Build posts array from groups
+		const posts = [];
+		const formData = new FormData();
+		let uploadMap = [];
+
+		// Process each group
+		for (const group of fieldGroups) {
+			const post = {
+				images: [],
+				fields: {}
+			};
+
+			// Add group metadata
+			for (let [name, value] of Object.entries(group.changes)) {
+				post.fields[name] = value;
+			}
+
+			// Get uploads for this group
+			const groupUploadIds = uploadIds.filter(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				return upload?.groupId === group.id;
+			});
+
+			// Add files for this group
+			for (const uploadId of groupUploadIds) {
+				const file = await this.getBlobData(uploadId);
+				if (file) {
+					formData.append('files[]', file);
+
+					const imageData = {
+						upload_id: uploadId,
+						index: uploadMap.length
+					};
+
+					// Check if featured
+					const uploadEl = this.uploadElements.get(uploadId);
+					const radioInput = uploadEl?.element?.querySelector('[name="featured"]');
+					if (radioInput?.checked) {
+						post.fields.featured = uploadId;
+					}
+
+					post.images.push(imageData);
+					uploadMap.push(uploadId);
+				}
+			}
+
+			posts.push(post);
+		}
+
+		// Handle remaining uploads (without groupId) - each becomes its own post
+		const remainingUploadIds = uploadIds.filter(uploadId => {
+			const upload = this.uploadStore.get(uploadId);
+			return !upload?.groupId;
+		});
+
+		for (const uploadId of remainingUploadIds) {
+			const post = {
+				images: [],
+				fields: {}
+			};
+
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				formData.append('files[]', file);
+
+				const imageData = {
+					upload_id: uploadId,
+					index: uploadMap.length
+				};
+				post.images.push(imageData);
+				uploadMap.push(uploadId);
+			}
+
+			posts.push(post);
+		}
+
+		// Add metadata to FormData
+		formData.append('content', fieldData.config.content);
+		formData.append('user', fieldData.config.itemID);
+		formData.append('posts', JSON.stringify(posts));
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+
+		const operation = {
+			endpoint: 'uploads/groups',
+			method: 'POST',
+			data: formData,
+			title: `Creating ${posts.length} ${fieldData.config.content}${posts.length > 1 ? 's' : ''} from uploads...`,
+			popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
+			canMerge: false,
+			headers: {
+				'action_nonce': window.auth.getNonce('dash')
+			},
+			append: '_upload',
+		};
+
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			// Update upload statuses
+			uploadIds.forEach(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				if (upload) {
+					upload.operationId = operationId;
+					upload.status = 'queued';
+					this.uploadStore.save(upload);
+					this.updateUploadStatus(uploadId, 'queued');
+				}
+			});
+
+			fieldData.operationId = operationId;
+			await this.saveFieldData(fieldData);
+
+			this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''} from your uploads`);
+
+			return operationId;
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			throw error;
+		}
+	}
+
+	async queueUpload(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.uploads || fieldData.uploads.size === 0) return;
+
+		const uploads = Array.from(fieldData.uploads);
+		const data = this.prepareUploadData(fieldData, uploads);
+
+		this.a11y.announce('Queuing for upload');
+
+		const operation = {
+			endpoint: 'uploads',
+			method: 'POST',
+			data: data,
+			title: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''} to server...`,
+			popup: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
+			canMerge: false,
+			headers: { 'action_nonce': window.auth.getNonce('dash') },
+			append: '_upload'
+		};
+
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			// Update upload statuses
+			uploads.forEach(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				if (upload) {
+					upload.operationId = operationId;
+					upload.status = 'queued';
+					this.uploadStore.save(upload);
+					this.updateUploadStatus(uploadId, 'queued');
+				}
+			});
+
+			fieldData.operationId = operationId;
+			await this.saveFieldData(fieldData);
+
+			return operationId;
+		} catch (error) {
+			throw error;
+		}
+	}
+
+	async prepareUploadData(fieldData, uploads) {
+		const formData = new FormData();
+		formData.append('content', fieldData.config.content);
+		formData.append('mode', fieldData.config.mode);
+		formData.append('field_name', fieldData.config.name);
+		formData.append('fieldId', fieldData.id);
+		formData.append('field_type', fieldData.config.type);
+		formData.append('subtype', fieldData.config.subtype);
+		formData.append('item_id', fieldData.config.itemID);
+		formData.append('destination', fieldData.config.destination || 'meta');
+
+		let uploadMap = [];
+
+
+		const blobPromises = uploads.map(async (uploadId) => {
+			const upload = this.uploadStore.get(uploadId);
+			if (!upload) return;
+
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				formData.append('files[]', file);
+				uploadMap.push(upload.id);
+			}
+		});
+
+		await Promise.all(blobPromises);
+
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+		return formData;
+	}
+
+	async queueUploadMeta(e) {
+		const uploadId = this.getUploadIdFromElement(e.target);
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload) return;
+
+		const fieldData = this.getFieldData(upload.fieldId);
+		if (!fieldData) return;
+
+		let data = {};
+		data[e.target.name] = e.target.value;
+
+		upload.meta = { ...upload.meta, ...data };
+		await this.uploadStore.save(upload);
+
+		let queueData = {};
+		queueData[upload.attachmentId ?? upload.id] = upload.meta;
+
+		const operation = {
+			endpoint: 'uploads/meta',
+			method: 'POST',
+			data: queueData,
+			title: 'Updating meta',
+			canMerge: true,
+			headers: { 'action_nonce': window.auth.getNonce('dash') }
+		};
+
+		try {
+			await this.queue.addToQueue(operation);
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'sendMetaUpdate',
+				uploadId: upload.id
+			});
+		}
+	}
+
+	/*******************************************************************************
+	 * QUEUE EVENT HANDLERS - CLEANUP AFTER SUCCESS
+	 *******************************************************************************/
+
+	/**
+	 * Handle successful operation completion - CLEAR STORES
+	 */
+	async handleOperationComplete(operation, fieldId) {
+		const results = operation.result?.data || operation.serverData?.data || [];
+
+		// Update upload statuses with attachment IDs
+		results.forEach(result => {
+			const upload = this.uploadStore.get(result.upload_id);
+			if (upload) {
+				upload.attachmentId = result.attachment_id;
+				upload.status = 'completed';
+				this.uploadStore.save(upload);
+				this.updateUploadStatus(result.upload_id, 'completed');
+			}
+		});
+
+		if (!fieldId) return;
+
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		// Clean up completed uploads from stores
+		const completedUploads = Array.from(fieldData.uploads).filter(uploadId => {
+			const upload = this.uploadStore.get(uploadId);
+			return upload?.status === 'completed';
+		});
+
+		for (const uploadId of completedUploads) {
+			await this.clearUpload(uploadId, false);
+			fieldData.uploads.delete(uploadId);
+		}
+
+		// If all uploads complete, clear entire field from stores
+		if (fieldData.uploads.size === 0) {
+			await this.clearFieldFromStores(fieldId);
+			this.a11y.announce('All uploads completed successfully');
+		} else {
+			// Otherwise just update field state
+			await this.saveFieldData(fieldData);
+		}
+
+		this.updateFieldState(fieldId);
+	}
+
+	/**
+	 * Handle operation failure
+	 */
+	handleOperationFailed(operation, fieldId) {
+		const uploadIds = operation.data instanceof FormData
+			? JSON.parse(operation.data.get('upload_ids') || '[]')
+			: operation.data.upload_ids || [];
+
+		uploadIds.forEach(uploadId => {
+			const upload = this.uploadStore.get(uploadId);
+			if (upload) {
+				upload.status = operation.status === 'operation-failed-permanent'
+					? 'failed_permanent'
+					: 'failed';
+				this.uploadStore.save(upload);
+				this.updateUploadStatus(uploadId, upload.status);
+			}
+		});
+
+		if (fieldId) {
+			this.updateFieldState(fieldId);
+		}
+	}
+
+	/**
+	 * Handle operation cancellation
+	 */
+	async handleOperationCancelled(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		const uploadsArray = fieldData.uploads instanceof Set
+			? Array.from(fieldData.uploads)
+			: fieldData.uploads;
+
+		for (const uploadId of uploadsArray) {
+			await this.clearUpload(uploadId, false);
+		}
+
+		await this.clearFieldFromStores(fieldId);
+		this.updateFieldState(fieldId);
+		this.a11y.announce('Upload cancelled');
+	}
+
+	getFieldGroups(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.groups) return [];
+
+		return fieldData.groups.map(group => ({
+			id: group.id,
+			uploads: group.uploads || [],
+			changes: group.changes || {}
+		}));
+	}
+
+	getSelectedRestorationUploads(notificationEl) {
+		let selected = [];
+		const checkboxes = notificationEl.querySelectorAll('[type=checkbox]:checked');
+
+		checkboxes.forEach(checkbox => {
+			const item = checkbox.closest('.item');
+			if (item) {
+				selected.push({
+					uploadId: item.dataset.uploadId,
+					fieldId: item.dataset.fieldId
+				});
+			}
+		});
+
+		return selected;
+	}
+
+	async restoreSelectedUploads(selectedUploads) {
+		const byField = new Map();
+		selectedUploads.forEach(item => {
+			if (!byField.has(item.fieldId)) {
+				byField.set(item.fieldId, []);
+			}
+			byField.get(item.fieldId).push(item.uploadId);
+		});
+
+		for (const [fieldId, uploadIds] of byField.entries()) {
+			const fieldState = this.fieldStore.get(fieldId);
+			if (fieldState) {
+				fieldState.uploads = uploadIds;
+				await this.restoreField(fieldState);
+			}
+		}
+	}
+
+	async restoreField(fieldState) {
+		const { config, context, uploads, groups, id } = fieldState;
+
+		// If in a modal, open it first
+		if (context?.modalType) {
+			await this.openModalForRestore(context);
+		}
+
+		// Find field element
+		let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
+
+		if (!fieldElement) {
+			const uploaderKey = `${config.content}_${config.itemID}_${config.name}`;
+			fieldElement = document.querySelector(`.field.upload[data-uploader="${uploaderKey}"]`);
+		}
+
+		if (!fieldElement) {
+			console.warn(`Field ${config.name} not found for restoration`, config);
+			return;
+		}
+
+		// Register the field if not already registered
+		let fieldKey = fieldElement.dataset.uploader;
+		if (!fieldKey || !this.fieldElements.has(fieldKey)) {
+			fieldKey = this.registerUploader(fieldElement);
+		}
+
+		const fieldEl = this.fieldElements.get(fieldKey);
+		const fieldData = this.getFieldData(fieldKey);
+
+		if (!fieldEl || !fieldData) {
+			console.error('Failed to register field for restoration');
+			return;
+		}
+
+		// Merge saved state back into field
+		fieldData.state = fieldState.state || 'ready';
+
+		// Rebuild UI references if needed
+		if (!fieldEl.ui) {
+			fieldEl.ui = this.buildFieldUI(fieldElement);
+		}
+
+		if (fieldEl.ui.groups?.display) {
+			fieldEl.ui.groups.display.hidden = false;
+		}
+		if (fieldEl.ui.dropZone) {
+			fieldEl.ui.dropZone.hidden = true;
+		}
+
+		// Restore groups first
+		if (groups && groups.length > 0) {
+			await this.restoreGroups(fieldKey, groups);
+		}
+
+		// Handle both Array and Set for uploads
+		const uploadsArray = uploads instanceof Set
+			? Array.from(uploads)
+			: Array.isArray(uploads)
+				? uploads
+				: [];
+
+		// Restore uploads
+		for (const uploadId of uploadsArray) {
+			// Get upload data from store
+			const uploadData = this.uploadStore.get(uploadId);
+			if (uploadData) {
+				await this.restoreUpload(fieldKey, uploadData);
+			}
+		}
+
+		// Update field state
+		await this.saveFieldData(fieldData);
+		this.updateFieldState(fieldKey);
+		this.maybeLockUploads(fieldKey);
+		this.refreshSortable(fieldKey);
+
+		// Queue for upload if needed
+		console.log(config);
+		if (config.autoUpload && config.mode === 'direct' && config.destination !== 'post_group') {
+			await this.queueUpload(fieldKey);
+		}
+	}
+
+	async restoreUpload(fieldId, uploadData) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+
+		if (!fieldEl || !fieldData) {
+			console.error('Field not found for upload restoration:', fieldId);
+			return;
+		}
+
+		// Get reconstructed File from blob data
+		const file = await this.getBlobData(uploadData.id);
+
+		if (!file) {
+			console.warn('Blob data not found for upload:', uploadData.id);
+			return;
+		}
+
+		// Create preview URL
+		const previewUrl = this.createPreviewUrl(file);
+
+		// Recreate DOM element
+		const subtype = this.getSubtypeFromMime(file.type);
+		const element = this.createUploadElement({
+			id: uploadData.id,
+			preview: previewUrl,
+			meta: uploadData.meta || {
+				originalName: file.name,
+				size: file.size,
+				type: file.type
+			},
+			subtype: subtype
+		}, fieldData.config.destination === 'post_group');
+
+		// Determine correct location
+		let location;
+		if (uploadData.groupId) {
+			// Check if group exists
+			const groupEl = this.groupElements.get(uploadData.groupId);
+			if (groupEl?.grid) {
+				location = groupEl.grid;
+
+				// Add to group's upload list
+				const group = fieldData.groups?.find(g => g.id === uploadData.groupId);
+				if (group) {
+					if (!group.uploads) group.uploads = [];
+					if (!group.uploads.includes(uploadData.id)) {
+						group.uploads.push(uploadData.id);
+					}
+				}
+			} else {
+				// Group doesn't exist, add to preview
+				location = fieldEl.ui.preview;
+				uploadData.groupId = null;
+			}
+		} else {
+			// No group, add to preview
+			location = fieldEl.ui.preview;
+		}
+
+		// Add element to DOM
+		if (location) {
+			location.appendChild(element);
+		} else if (fieldEl.ui.preview) {
+			fieldEl.ui.preview.appendChild(element);
+			location = fieldEl.ui.preview;
+		}
+
+		// Store runtime element data
+		this.uploadElements.set(uploadData.id, {
+			element: element,
+			preview: previewUrl,
+			location: location
+		});
+
+		// Add to field uploads
+		if (!fieldData.uploads) fieldData.uploads = new Set();
+		fieldData.uploads.add(uploadData.id);
+
+		// Update upload data in store
+		uploadData.status = 'processed';
+		await this.uploadStore.save(uploadData);
+
+		// Update sortable state for the grid
+		if (location) {
+			this.updateSortableState(location);
+		}
+	}
+
+	async restoreGroups(fieldId, groups) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+
+		if (!fieldEl || !fieldData) {
+			console.error('Field not found for group restoration:', fieldId);
+			return;
+		}
+
+		for (const groupData of groups) {
+			const group = this.createGroup(fieldId, groupData.id);
+			if (!group) {
+				console.warn('Failed to create group:', groupData.id);
+				continue;
+			}
+
+			const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
+			if (storedGroup) {
+				// Restore metadata
+				if (groupData.changes) {
+					storedGroup.changes = { ...groupData.changes };
+				}
+
+				// Preserve upload order
+				if (groupData.uploads) {
+					storedGroup.uploads = [...groupData.uploads];
+				}
+
+				// Restore form field values
+				if (groupData.changes) {
+					const titleInput = group.element.querySelector('[name*="post_title"]');
+					const excerptInput = group.element.querySelector('[name*="post_excerpt"]');
+
+					if (titleInput && groupData.changes.post_title) {
+						titleInput.value = groupData.changes.post_title;
+					}
+					if (excerptInput && groupData.changes.post_excerpt) {
+						excerptInput.value = groupData.changes.post_excerpt;
+					}
+				}
+			}
+		}
+
+		await this.saveFieldData(fieldData);
+	}
+
+	async openModalForRestore(context) {
+		if (!context) return;
+
+		const { modalType, itemId } = context;
+
+		// Find and click the appropriate button to open the modal
+		let trigger = null;
+
+		switch(modalType) {
+			case 'create':
+				trigger = document.querySelector('[data-action="create"]');
+				break;
+			case 'edit':
+				// Need to find the specific edit button
+				if (itemId) {
+					trigger = document.querySelector(`[data-action="edit"][data-id="${itemId}"]`);
+				}
+				break;
+			case 'bulkEdit':
+				trigger = document.querySelector('[data-action="bulk-edit"]');
+				break;
+		}
+
+		if (trigger) {
+			trigger.click();
+
+			// Wait for modal to open and render
+			await new Promise(resolve => setTimeout(resolve, 300));
+		} else {
+			console.warn('Modal trigger not found for restoration:', context);
+		}
+	}
+
+	formatBytes(bytes, decimals = 2) {
+		if (bytes === 0) return '0 Bytes';
+		const k = 1024;
+		const dm = decimals < 0 ? 0 : decimals;
+		const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+		const i = Math.floor(Math.log(bytes) / Math.log(k));
+		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+	}
+
+	/*******************************************************************************
+	 * CLEANUP METHODS - AGGRESSIVE CLEANUP AFTER SUCCESS
+	 *******************************************************************************/
+
+	/**
+	 * Clear individual upload from stores (called after successful upload)
+	 */
+	async clearUpload(uploadId, persist = true) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (uploadEl) {
+			this.revokePreviewUrl(uploadEl.preview);
+			if (uploadEl.element) {
+				const previewUrl = uploadEl.element.dataset.previewUrl;
+				this.revokePreviewUrl(previewUrl);
+				delete uploadEl.element.dataset.previewUrl;
+			}
+		}
+
+		// Remove from runtime memory
+		this.uploadElements.delete(uploadId);
+
+		// Remove from store (no separate blob store - it's part of the upload object)
+		await this.uploadStore.delete(uploadId);
+
+		// Update field if needed
+		if (persist) {
+			const upload = this.uploadStore.get(uploadId);
+			if (upload?.fieldId) {
+				await this.schedulePersistance(upload.fieldId);
+			}
+		}
+	}
+
+	/**
+	 * Clear entire field from stores (called when all uploads complete)
+	 */
+	async clearFieldFromStores(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+
+		// Clear all related uploads
+		if (fieldData?.uploads) {
+			const uploadsArray = fieldData.uploads instanceof Set
+				? Array.from(fieldData.uploads)
+				: fieldData.uploads;
+
+			for (const uploadId of uploadsArray) {
+				await this.uploadStore.delete(uploadId);
+			}
+		}
+
+		// Clear field from store
+		await this.fieldStore.delete(fieldId);
+
+		// Keep runtime references (fieldElements, etc) intact for reuse
+	}
+
+	cleanupAllPreviewUrls() {
+		if (this.previewUrls) {
+			this.previewUrls.forEach(url => {
+				try {
+					URL.revokeObjectURL(url);
+				} catch (e) {
+					// Ignore errors during cleanup
+				}
+			});
+			this.previewUrls.clear();
+		}
+	}
+
+	/*******************************************************************************
+	 * UI UPDATE METHODS
+	 *******************************************************************************/
+
+	updateFieldState(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldEl || !fieldData) return;
+
+		const container = fieldEl.element;
+		const uploadCount = fieldData.uploads?.size || 0;
+		const hasGroups = fieldEl.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
+
+		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
+		container.dataset.uploadCount = uploadCount.toString();
+		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
+
+		if (fieldEl.ui.preview) {
+			fieldEl.ui.preview.setAttribute('aria-label',
+				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
+			);
+		}
+	}
+
+	updateUploadProgress(fieldId, current, total, message) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl?.ui?.progress?.progress) return;
+
+		const progress = fieldEl.ui.progress;
+		const percent = total > 0 ? (current / total) * 100 : 0;
+
+		if (progress.fill) progress.fill.style.width = `${percent}%`;
+		if (progress.text) progress.text.textContent = message;
+		if (progress.count) progress.count.textContent = `${current}/${total}`;
+
+		progress.progress.hidden = (current === total);
+	}
+
+	updateFieldStatus(fieldId, status) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		fieldData.state = status;
+		this.saveFieldData(fieldData);
+	}
+
+	updateUploadStatus(uploadId, status) {
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload) return;
+
+		upload.status = status;
+		this.uploadStore.save(upload);
+		this.updateUploadUI(uploadId);
+	}
+
+	updateUploadUI(uploadId) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload || !uploadEl?.element) return;
+
+		uploadEl.element.className = uploadEl.element.className.replace(/status-[\w-]+/g, '');
+		uploadEl.element.classList.add(`status-${upload.status}`);
+
+		const progress = uploadEl.element.querySelector('.progress');
+		if (progress) {
+			this.updateUploadItemProgress(uploadId,
+				this.getStatusProgress(upload.status),
+				upload.status
+			);
+		}
+	}
+
+	showUploadProgress(uploadId, show = true) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!uploadEl?.element) return;
+
+		const progressEl = uploadEl.element.querySelector('.progress');
+		if (progressEl) {
+			if (show) {
+				progressEl.style.removeProperty('animation');
+				progressEl.hidden = false;
+			} else {
+				progressEl.style.animation = 'fadeOut var(--transition-base)';
+				setTimeout(() => { progressEl.hidden = true; }, 300);
+			}
+		}
+	}
+
+	updateUploadItemProgress(uploadId, percent, status = null) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!uploadEl?.element) return;
+
+		const progressEl = uploadEl.element.querySelector('.progress');
+		if (!progressEl) return;
+
+		const fill = progressEl.querySelector('.fill');
+		const details = progressEl.querySelector('.details');
+		const icon = progressEl.querySelector('.icon');
+
+		if (fill) fill.style.width = `${percent}%`;
+		if (status && details) details.textContent = this.getStatusText(status);
+		if (status && icon) icon.innerHTML = this.getStatusIcon(status).outerHTML;
+	}
+
+	maybeLockUploads(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldEl?.ui?.dropZone || !fieldData) return;
+
+		const uploadCount = fieldData.uploads?.size || 0;
+
+		// For groupable uploads, set max to 20
+		const maxFiles = fieldData.config.destination === 'post_group'
+			? 20
+			: (fieldData.config?.maxFiles || 999);
+
+		fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
+		fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
+
+		// Show helpful message for groupable uploads
+		if (fieldData.config.destination === 'post_group' && uploadCount >= maxFiles) {
+			this.a11y.announce('Maximum of 20 uploads reached. Please submit current uploads before adding more.');
+		}
+	}
+
+	/*******************************************************************************
+	 * GROUP MANAGEMENT
+	 *******************************************************************************/
+	/**
+	 * Create sortable instance for a grid
+	 */
+	createSortableForGrid(grid, fieldId, groupId = null) {
+		if (!grid || grid.sortableInstance) return;
+
+		const sortableInstance = new Sortable(grid, {
+			animation: 150,
+			draggable: '.item',
+			multiDrag: true,
+			selectedClass: 'selected-for-drag',
+			avoidImplicitDeselect: true,
+			group: { name: fieldId, pull: true, put: true },
+			ghostClass: 'sortable-ghost',
+			chosenClass: 'sortable-chosen',
+			dragClass: 'sortable-drag',
+
+			// Centralized drop handler
+			onEnd: (evt) => this.handleDrop(evt, fieldId),
+
+			// Selection sync
+			onSelect: (evt) => {
+				const checkbox = evt.item.querySelector('[name*="select-item"]');
+				if (checkbox && !checkbox.checked) {
+					checkbox.checked = true;
+					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+				}
+			},
+
+			onDeselect: (evt) => {
+				const checkbox = evt.item.querySelector('[name*="select-item"]');
+				if (checkbox && checkbox.checked) {
+					checkbox.checked = false;
+					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+				}
+			},
+
+			onAdd: (evt) => this.updateSortableState(evt.to),
+			onRemove: (evt) => this.updateSortableState(evt.from)
+		});
+
+		grid.sortableInstance = sortableInstance;
+
+		const gridId = groupId
+			? `${fieldId}-group-${groupId}`
+			: `${fieldId}-preview`;
+
+		this.sortableInstances.set(gridId, sortableInstance);
+
+		return sortableInstance;
+	}
+	createGroup(fieldId, groupId = null) {
+		const fieldData = this.getFieldData(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldData || !fieldEl) return null;
+
+		if (!groupId) {
+			groupId = `group_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
+		}
+
+		const groupElement = this.createGroupElement(groupId, fieldId);
+		if (!groupElement) return null;
+
+		// Store in field UI Map
+		if (!fieldEl.ui.groups) {
+			fieldEl.ui.groups = {
+				groups: new Map(),
+				container: null,
+				empty: null,
+				display: null
+			};
+		}
+
+		fieldEl.ui.groups.groups.set(groupId, groupElement);
+
+		// Insert into DOM
+		if (fieldEl.ui.groups.container && fieldEl.ui.groups.empty) {
+			fieldEl.ui.groups.container.insertBefore(groupElement, fieldEl.ui.groups.empty);
+		} else if (fieldEl.ui.groups.container) {
+			fieldEl.ui.groups.container.appendChild(groupElement);
+		}
+
+		// Store group element reference
+		const grid = groupElement.querySelector('.item-grid.group');
+		this.groupElements.set(groupId, {
+			element: groupElement,
+			grid: grid,
+			fieldId: fieldId
+		});
+
+		// Add to field groups
+		if (!fieldData.groups) fieldData.groups = [];
+		const existingGroup = fieldData.groups.find(g => g.id === groupId);
+		if (!existingGroup) {
+			fieldData.groups.push({
+				id: groupId,
+				uploads: [],
+				changes: {}
+			});
+			this.saveFieldData(fieldData);
+		}
+
+		// Initialize selection handler
+		this.addGroupSelectionHandler(fieldId, groupId);
+
+		if (grid) {
+			this.createSortableForGrid(grid, fieldId, groupId);
+		}
+
+		return { id: groupId, element: groupElement, grid: grid };
+	}
+	createGroupElement(groupId, fieldId) {
+		let groupElement = window.getTemplate('imageGroup');
+		if (!groupElement) return;
+
+		groupElement.dataset.groupId = groupId;
+		groupElement.dataset.fieldId = fieldId;
+
+		let fields = window.getTemplate('groupMetadata');
+		const fieldsContainer = groupElement.querySelector('.fields');
+		if (fieldsContainer && fields) {
+			fieldsContainer.append(fields);
+
+			const titleInput = fieldsContainer.querySelector('[name="post_title"]');
+			const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
+
+			if (titleInput) {
+				titleInput.id = `${groupId}_title`;
+				titleInput.name = `${groupId}[post_title]`;
+			}
+			if (excerptInput) {
+				excerptInput.id = `${groupId}_excerpt`;
+				excerptInput.name = `${groupId}[post_excerpt]`;
+			}
+
+			const fieldData = this.getFieldData(fieldId);
+			if (fieldData && fieldData.config.content !== '') {
+				let summary = groupElement.querySelector('summary');
+				if (summary) summary.textContent = fieldData.config.content + ' Fields';
+			}
+		} else {
+			groupElement.querySelector('details')?.remove();
+		}
+
+		const gridContainer = groupElement.querySelector('.item-grid.group');
+		if (gridContainer) {
+			gridContainer.dataset.groupId = groupId;
+		}
+
+		return groupElement;
+	}
+
+	deleteGroup(groupId, confirm = true) {
+		const groupEl = this.groupElements.get(groupId);
+		if (!groupEl) return;
+
+		const fieldData = this.getFieldData(groupEl.fieldId);
+		if (!fieldData) return;
+
+		const group = fieldData.groups?.find(g => g.id === groupId);
+		let keepUploads = true;
+
+		if (confirm && group?.uploads?.length > 0) {
+			keepUploads = !window.confirm('Delete uploads in group?');
+		}
+
+		if (confirm && keepUploads && group?.uploads) {
+			// Move uploads back to preview
+			group.uploads.forEach(uploadId => {
+				this.removeFromGroup(uploadId);
+			});
+		}
+
+		// Remove from field groups
+		if (fieldData.groups) {
+			fieldData.groups = fieldData.groups.filter(g => g.id !== groupId);
+			this.saveFieldData(fieldData);
+		}
+
+		// Remove DOM element
+		if (groupEl.element) {
+			groupEl.element.remove();
+			this.a11y.announce('Group removed');
+		}
+
+		// Remove from maps
+		this.groupElements.delete(groupId);
+
+		// Clean up sortable
+		const sortableKey = `${groupEl.fieldId}-group-${groupId}`;
+		const sortable = this.sortableInstances.get(sortableKey);
+		if (sortable?.destroy) {
+			sortable.destroy();
+		}
+		this.sortableInstances.delete(sortableKey);
+
+		this.schedulePersistance(groupEl.fieldId);
+	}
+
+	addToGroup(uploadId, target = null, persist = true) {
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!upload || !uploadEl) return;
+
+		const fieldData = this.getFieldData(upload.fieldId);
+		const fieldEl = this.fieldElements.get(upload.fieldId);
+		if (!fieldData || !fieldEl) return;
+
+		// Already in correct location
+		if ((!target && uploadEl.location === fieldEl.ui.preview) || target === uploadEl.location) {
+			return;
+		}
+
+		// Remove from previous group
+		if (upload.groupId) {
+			const group = fieldData.groups?.find(g => g.id === upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					this.deleteGroup(upload.groupId);
+				}
+			}
+		}
+
+		// Clear selection checkbox
+		const checkbox = uploadEl.element.querySelector('[name*="select-item"]');
+		if (checkbox) checkbox.checked = false;
+
+		let featured = uploadEl.element.querySelector('[name="featured"]');
+		if (featured) featured.hidden = !target;
+
+		// Moving to preview or to group
+		if (!target || target.classList.contains('preview')) {
+			target = fieldEl.ui.preview;
+			upload.groupId = null;
+		} else {
+			// Moving to group
+			const groupId = target.dataset.groupId;
+			if (featured) featured.name = groupId + '_' + featured.name;
+
+			const group = fieldData.groups?.find(g => g.id === groupId);
+			if (group) {
+				if (!group.uploads) group.uploads = [];
+				group.uploads.push(uploadId);
+				upload.groupId = groupId;
+			}
+		}
+
+		// Update location
+		uploadEl.location = target;
+		target.append(uploadEl.element);
+
+		// Update stores
+		this.uploadStore.save(upload);
+		if (persist) {
+			this.saveFieldData(fieldData);
+		}
+
+		// Update sortable state
+		this.updateSortableState(target);
+		if (uploadEl.location && uploadEl.location !== target) {
+			this.updateSortableState(uploadEl.location);
+		}
+	}
+
+	removeFromGroup(uploadId) {
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!upload || !uploadEl) return;
+
+		const fieldData = this.getFieldData(upload.fieldId);
+		const fieldEl = this.fieldElements.get(upload.fieldId);
+		if (!fieldData || !fieldEl) return;
+
+		// Remove from current group
+		if (upload.groupId) {
+			const group = fieldData.groups?.find(g => g.id === upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					this.deleteGroup(upload.groupId, false);
+				}
+			}
+			upload.groupId = null;
+		}
+
+		// Move back to preview
+		if (fieldEl.ui?.preview) {
+			fieldEl.ui.preview.appendChild(uploadEl.element);
+			uploadEl.location = fieldEl.ui.preview;
+		}
+
+		// Hide featured radio
+		const featured = uploadEl.element.querySelector('[name="featured"]');
+		if (featured) {
+			featured.hidden = true;
+			featured.checked = false;
+		}
+
+		this.uploadStore.save(upload);
+		this.updateSortableState(fieldEl.ui.preview);
+	}
+
+	removeUpload(fieldId, uploadId) {
+		const fieldData = this.getFieldData(fieldId);
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+
+		if (!fieldData || !upload) return;
+
+		// Remove from field
+		fieldData.uploads?.delete(uploadId);
+
+		// Remove from group if grouped
+		if (upload.groupId) {
+			const group = fieldData.groups?.find(g => g.id === upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					this.deleteGroup(upload.groupId);
+				}
+			}
+		}
+
+		// Clean up element
+		uploadEl?.element?.remove();
+
+		// Clean up memory
+		this.clearUpload(uploadId);
+
+		// Update field state
+		this.saveFieldData(fieldData);
+		this.updateFieldState(fieldId);
+		this.maybeLockUploads(fieldId);
+
+		const handler = this.selectionHandlers.get(fieldId);
+		if (handler) {
+			handler.deselect(uploadId);
+		}
+
+		this.a11y.announce('Upload removed');
+	}
+
+	handleGroupMetaChange(input) {
+		const groupEl = this.getGroupFromElement(input);
+		if (!groupEl) return;
+
+		const fieldData = this.getFieldData(groupEl.fieldId);
+		const group = fieldData?.groups?.find(g => g.id === groupEl.element.dataset.groupId);
+		if (!group) return;
+
+		if (!group.changes) group.changes = {};
+
+		let name = input.name;
+		if (name.includes('group')) {
+			name = name.replace(`${group.id}_`, '').replace(`${group.id}[`, '').replace(']', '');
+		}
+
+		group.changes[name] = input.value;
+		this.saveFieldData(fieldData);
+		this.schedulePersistance(groupEl.fieldId);
+	}
+
+	/*******************************************************************************
+	 * ACTION HANDLERS
+	 *******************************************************************************/
+
+	handleAction(button) {
+		const action = button.dataset.action;
+		const fieldId = this.getFieldIdFromElement(button);
+
+		switch(action) {
+			case 'add-to-group':
+				this.handleAddToGroup(button);
+				break;
+			case 'delete-group':
+				this.handleDeleteGroup(button);
+				break;
+			case 'delete-upload':
+			case 'remove-from-group':
+				this.handleRemoveItem(button);
+				break;
+			case 'upload':
+				const fieldEl = this.fieldElements.get(fieldId);
+				if (fieldEl) {
+					fieldEl.element.closest('details').open = false;
+					document.body.classList.add('uploading');
+					this.submitUploads(fieldId);
+				}
+				break;
+			case 'restore':
+				this.handleRestoreUploads().then(() => {});
+				break;
+			case 'restore-all':
+				this.handleRestoreAll().then(() => {});
+				break;
+			case 'clear-cache':
+				if (!confirm('Save these uploads for later?')) {
+					this.cleanupStoredUploads();
+				}
+				this.cleanupRestore();
+				break;
+		}
+	}
+
+	handleAddToGroup(button) {
+		const fieldElement = button.closest(this.selectors.field.field);
+		const fieldId = fieldElement?.dataset.uploader;
+		if (!fieldId) return;
+
+		const selected = this.selected.get(fieldId);
+
+		if (!selected || selected.size === 0) {
+			this.createGroup(fieldId);
+		} else {
+			const group = this.createGroup(fieldId);
+			if (!group) return;
+
+			selected.forEach(uploadId => {
+				this.addToGroup(uploadId, group.grid);
+			});
+
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+
+			this.a11y.announce(`Created group with ${selected.size} items`);
+		}
+
+		this.schedulePersistance(fieldId);
+	}
+
+	handleDeleteGroup(button) {
+		const group = button.closest(this.selectors.groups.container);
+		if (!group) return;
+
+		const groupId = group.dataset.groupId;
+		const fieldId = this.getFieldIdFromElement(group);
+
+		if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
+			return;
+		}
+
+		const items = group.querySelectorAll(this.selectors.items.item);
+		items.forEach(item => {
+			const uploadId = item.dataset.uploadId;
+			this.removeFromGroup(uploadId);
+		});
+
+		this.deleteGroup(groupId);
+		this.a11y.announce('Group deleted, items returned to upload area');
+		this.schedulePersistance(fieldId);
+	}
+
+	handleRemoveItem(button) {
+		const item = button.closest(this.selectors.items.item);
+		if (!item) return;
+
+		const uploadId = item.dataset.uploadId;
+		const fieldId = this.getFieldIdFromElement(item);
+
+		if (!confirm('Remove this item?')) return;
+
+		this.removeUpload(fieldId, uploadId);
+		this.a11y.announce('Item removed');
+		this.schedulePersistance(fieldId);
+	}
+
+	/*******************************************************************************
+	 * SELECTION MANAGEMENT
+	 *******************************************************************************/
+
+	addFieldSelectionHandler(fieldId) {
+		if (this.selectionHandlers.has(fieldId)) {
+			return this.selectionHandlers.get(fieldId);
+		}
+
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl?.element) return;
+
+		const handler = new window.jvbHandleSelection({
+			container: fieldEl.element,
+			ui: {
+				selectAll: fieldEl.element.querySelector('[name="select-all-uploads"]'),
+				bulkControls: fieldEl.element.querySelector('.selection-actions'),
+				count: fieldEl.element.querySelector('.selection-count')
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]'
+		});
+
+		handler.subscribe((event, data) => {
+			switch(event) {
+				case 'item-selected':
+					// Sync with Sortable
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'item-deselected':
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'range-selected':
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'select-all':
+					this.handleSelectAll(data.container, data.selected);
+					break;
+			}
+		});
+
+		this.selectionHandlers.set(fieldId, handler);
+		return handler;
+	}
+
+	addGroupSelectionHandler(fieldId, groupId) {
+		const handlerKey = `${fieldId}_${groupId}`;
+		if (this.selectionHandlers.has(handlerKey)) {
+			return this.selectionHandlers.get(handlerKey);
+		}
+
+		const groupEl = this.groupElements.get(groupId);
+		if (!groupEl?.element) return;
+
+		const handler = new window.jvbHandleSelection({
+			container: groupEl.element,
+			ui: {
+				selectAll: groupEl.element.querySelector(this.selectors.groups.selectAll),
+				bulkControls: groupEl.element.querySelector(this.selectors.groups.actions),
+				count: groupEl.element.querySelector(this.selectors.groups.count)
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]'
+		});
+
+		handler.subscribe((event, data) => {
+			switch(event) {
+				case 'item-selected':
+				case 'item-deselected':
+				case 'range-selected':
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'select-all':
+					this.handleSelectAll(data.container, data.selected);
+					break;
+			}
+		});
+
+		this.selectionHandlers.set(handlerKey, handler);
+		return handler;
+	}
+
+	handleSelectAll(container, selected) {
+		// Can add custom logic here if needed
+	}
+
+	/*******************************************************************************
+	 * HELPER METHODS
+	 *******************************************************************************/
+
+	/**
+	 * Get field data from store and normalize it
+	 * Always use this instead of directly accessing fieldStore.get()
+	 */
+	getFieldData(fieldId) {
+		const fieldData = this.fieldStore.get(fieldId);
+		if (!fieldData) return null;
+
+		// Only convert uploads back to Set (DataStore returns Arrays)
+		if (Array.isArray(fieldData.uploads)) {
+			fieldData.uploads = new Set(fieldData.uploads);
+		} else if (!fieldData.uploads) {
+			fieldData.uploads = new Set();
+		}
+
+		// Ensure groups is an array
+		if (!Array.isArray(fieldData.groups)) {
+			fieldData.groups = [];
+		}
+
+		return fieldData;
+	}
+
+	/**
+	 * Save field data to store, converting Sets to Arrays
+	 */
+	async saveFieldData(fieldData) {
+		await this.fieldStore.save({
+			...fieldData,
+			timestamp: Date.now()
+		});
+	}
+
+	determineFieldId(fieldElement) {
+		const content = fieldElement.dataset.content+'_' ||
+			fieldElement.closest('dialog')?.dataset.content+'_' ||
+			fieldElement.closest('form')?.dataset.save+'_' || '';
+		const itemID = fieldElement.dataset.itemId+'_' ||
+			fieldElement.closest('dialog')?.dataset.itemId+'_' || '';
+		const field = fieldElement.dataset.field || '';
+
+		return `${content}${itemID}${field}`;
+	}
+
+	getFromElement(element, type) {
+		const map = {
+			'field': {
+				selector: this.selectors.field.field,
+				key: 'uploader',
+				getRuntimeData: (id) => this.fieldElements.get(id),
+				getStoreData: (id) => this.getFieldData(id)
+			},
+			'upload': {
+				selector: this.selectors.items.item,
+				key: 'uploadId',
+				getRuntimeData: (id) => this.uploadElements.get(id),
+				getStoreData: (id) => this.uploadStore.get(id)
+			},
+			'group': {
+				selector: this.selectors.groups.container,
+				key: 'groupId',
+				getRuntimeData: (id) => this.groupElements.get(id),
+				getStoreData: (id) => {
+					// Groups are stored in field.groups array
+					const groupEl = this.groupElements.get(id);
+					if (!groupEl) return null;
+					const fieldData = this.getFieldData(groupEl.fieldId);
+					return fieldData?.groups?.find(g => g.id === id);
+				}
+			}
+		};
+
+		const config = map[type];
+		if (!config) return null;
+
+		const el = element.closest(config.selector);
+		if (!el) return null;
+
+		const id = el.dataset[config.key];
+
+		// Return combined runtime + store data for convenience
+		const runtime = config.getRuntimeData(id);
+		const store = config.getStoreData(id);
+
+		return { ...runtime, ...store };
+	}
+
+	getFieldFromElement(el) { return this.getFromElement(el, 'field'); }
+	getUploadFromElement(el) { return this.getFromElement(el, 'upload'); }
+	getGroupFromElement(el) { return this.getFromElement(el, 'group'); }
+
+	getFieldIdFromElement(el) {
+		const field = this.getFromElement(el, 'field');
+		return field?.id ?? null;
+	}
+	getUploadIdFromElement(el) {
+		const upload = this.getFromElement(el, 'upload');
+		return upload?.id ?? null;
+	}
+	getGroupIdFromElement(el) {
+		const group = this.getFromElement(el, 'group');
+		return group?.id ?? null;
+	}
+
+	getSubtypeFromMime(mimeType) {
+		if (mimeType.startsWith('image/')) return 'image';
+		if (mimeType.startsWith('video/')) return 'video';
+		return 'document';
+	}
+
+	getStatusText(status) {
+		return this.statusMapping[status] || status;
+	}
+
+	getStatusIcon(status) {
+		return window.getIcon(this.queue.icons[status]);
+	}
+
+	getStatusProgress(status) {
+		const progress = {
+			'local_processing': 28,
+			'queued': 50,
+			'uploading': 66,
+			'pending': 75,
+			'processing': 89,
+			'completed': 100
+		};
+		return progress[status] || 0;
+	}
+
+
+	createUploadElement(upload, draggable = false) {
+		let image = window.getTemplate('uploadItem');
+		if (!image) return;
+
+		image.dataset.uploadId = upload.id;
+		image.dataset.subtype = upload.subtype || 'image';
+
+		let [featured, img, video, preview, details] = [
+			image.querySelector('[name="featured"]'),
+			image.querySelector('img'),
+			image.querySelector('video'),
+			image.querySelector('label > span'),
+			image.querySelector('details')
+		];
+
+		if (featured) featured.value = upload.id;
+
+		switch (upload.subtype) {
+			case 'image':
+				if (img) {
+					img.src = upload.preview;
+					img.alt = upload.meta?.originalName || '';
+				}
+				video?.remove();
+				preview?.remove();
+				break;
+			case 'video':
+				if (video) video.src = upload.preview;
+				img?.remove();
+				preview?.remove();
+				break;
+			case 'document':
+				const fileName = upload.meta?.originalName || '';
+				const extension = fileName.split('.').pop()?.toLowerCase() || '';
+				const iconMap = {
+					'pdf': 'file-pdf', 'csv': 'file-csv',
+					'doc': 'file-doc', 'docx': 'file-doc',
+					'txt': 'file-txt', 'xls': 'file-xls', 'xlsx': 'file-xls'
+				};
+				const icon = window.getIcon(iconMap[extension] || 'file');
+				if (preview) {
+					preview.innerText = fileName;
+					preview.prepend(icon);
+				}
+				img?.remove();
+				video?.remove();
+				break;
+		}
+
+		if (details) {
+			let template = window.getTemplate('uploadMeta');
+			if (template) details.append(template);
+		}
+
+		image.draggable = draggable;
+
+		// Update input IDs
+		image.querySelectorAll('input').forEach(input => {
+			let id = input.id;
+			if (id) {
+				let newId = id + upload.id;
+				let label = input.parentNode.querySelector(`label[for="${id}"]`);
+				input.id = newId;
+				if (label) label.htmlFor = newId;
+			}
+		});
+
+		return image;
+	}
+
+	/*******************************************************************************
+	 * PERSISTENCE
+	 *******************************************************************************/
+
+	schedulePersistance(fieldId) {
+		const key = `persist_${fieldId}`;
+		window.debouncer.schedule(
+			key,
+			() => this.persistFieldState(fieldId),
+			250
+		);
+	}
+
+	async persistFieldState(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		// Save with updated timestamp
+		await this.saveFieldData(fieldData);
+	}
+
+	// In UploadManager, add blob conversion helpers
+	async saveBlobData(uploadId, file) {
+		const arrayBuffer = await file.arrayBuffer();
+
+		const uploadData = this.uploadStore.get(uploadId) || { id: uploadId };
+
+		// Store blob data as ArrayBuffer with metadata
+		uploadData.blobData = {
+			buffer: arrayBuffer,
+			name: file.name,
+			type: file.type,
+			size: file.size,
+			lastModified: file.lastModified || Date.now()
+		};
+
+		await this.uploadStore.save(uploadData);
+	}
+
+	async getBlobData(uploadId) {
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload?.blobData) return null;
+
+		// Reconstruct File from ArrayBuffer
+		const blob = new Blob([upload.blobData.buffer], { type: upload.blobData.type });
+		return new File([blob], upload.blobData.name, {
+			type: upload.blobData.type,
+			lastModified: upload.blobData.lastModified
+		});
+	}
+
+	/*******************************************************************************
+	 HELPER to GET UPLOADED FILES
+	*******************************************************************************/
+	/**
+	 * Get all files for a form (searches all upload fields within form)
+	 */
+	async getFilesForForm(formElement) {
+		const uploadFields = formElement.querySelectorAll('[data-upload-field]');
+		const allFiles = [];
+
+		for (const field of uploadFields) {
+			const fieldId = this.determineFieldId(field);
+			const files = await this.getFilesForField(fieldId);
+			allFiles.push(...files);
+		}
+
+		return allFiles;
+	}
+
+	/**
+	 * Get all files for a specific field
+	 */
+	async getFilesForField(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.uploads) return [];
+
+		const files = [];
+		const uploadsArray = fieldData.uploads instanceof Set
+			? Array.from(fieldData.uploads)
+			: fieldData.uploads;
+
+		for (const uploadId of uploadsArray) {
+			const upload = this.uploadStore.get(uploadId);
+			if (!upload) continue;
+
+			// Get the actual File object from blob data
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				files.push({
+					file: file,
+					uploadId: uploadId,
+					fieldName: fieldData.config.name,
+					meta: upload.meta || {}
+				});
+			}
+		}
+
+		return files;
+	}
+
+	/*******************************************************************************
+	 * RECOVERY & RESTORATION
+	 *******************************************************************************/
+
+	handleFieldStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				this.fieldStoreReady = true;
+				this.checkIfBothStoresReady();
+				break;
+		}
+	}
+
+	handleUploadStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				this.uploadStoreReady = true;
+				this.checkIfBothStoresReady();
+				break;
+			case 'item-saved':
+				this.showSaveIndicator(data.key);
+				break;
+		}
+	}
+
+	checkIfBothStoresReady() {
+		if (this.fieldStoreReady && this.uploadStoreReady && !this.hasCheckedForUploads) {
+			this.hasCheckedForUploads = true;
+			this.checkForStoredUploads();
+		}
+	}
+
+	async checkForStoredUploads() {
+		const allFieldStates = this.fieldStore.getAll();
+
+		const pendingFields = allFieldStates.filter(field => {
+			if (!field.uploads) return false;
+
+			// Handle both Set and Array (from IndexedDB)
+			const uploadsArray = field.uploads instanceof Set
+				? Array.from(field.uploads)
+				: Array.isArray(field.uploads)
+					? field.uploads
+					: [];
+
+			return uploadsArray.some(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				return upload && !upload.operationId &&
+					['completed', 'processed', 'local_processing', 'processed-original'].includes(upload.status);
+			});
+		});
+		if (pendingFields.length === 0) return;
+
+		await this.showRecoveryNotification(pendingFields);
+	}
+
+	async showRecoveryNotification(pendingFields) {
+		const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
+		const totalGroups = pendingFields.reduce((sum, field) =>
+			sum + (field.groups?.length || 0), 0);
+
+		let notification = window.getTemplate('restoreNotification');
+		if (!notification) {
+			console.error('Restore notification template not found');
+			return;
+		}
+
+		// Build appropriate message
+		let message;
+		if (totalGroups > 0) {
+			let group = totalGroups > 1 ? 'groups' : 'group';
+			let upload = totalUploads > 1 ? 'uploads' : 'upload';
+			message = `${totalGroups} ${group} with ${totalUploads} ${upload} can be restored.`;
+		} else {
+			message = `${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`;
+		}
+
+		const detailsEl = notification.querySelector('.restore-details');
+		if (detailsEl) {
+			detailsEl.textContent = message;
+		}
+
+		// Build the restoration preview
+		for (const field of pendingFields) {
+			let fieldTemplate = window.getTemplate('restoreField');
+			if (!fieldTemplate) continue;
+
+			// Set field name/title
+			const titleEl = fieldTemplate.querySelector('h3');
+			if (titleEl) {
+				titleEl.textContent = field.config.name || 'Unnamed Field';
+			}
+
+			const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
+
+			// Process each upload
+			for (let uploadId of field.uploads) {
+				const upload = this.uploadStore.get(uploadId);
+				let uploadItem = window.getTemplate('uploadItem');
+				if (!uploadItem) continue;
+				//
+				// 	const imgEl = uploadItem.querySelector('img');
+				// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
+				//
+				const file = await this.getBlobData(upload.id);
+				if (file) {
+
+					try {
+						// Create new blob URL from stored data
+						const previewUrl = this.createPreviewUrl(file);
+
+						let [
+							featured,
+							img,
+							video,
+							preview,
+							details
+						] = [
+							uploadItem.querySelector('[name="featured"]'),
+							uploadItem.querySelector('img'),
+							uploadItem.querySelector('video'),
+							uploadItem.querySelector('label > span'),
+							uploadItem.querySelector('details')
+						];
+
+						uploadItem.dataset.uploadId = upload.id;
+
+
+						uploadItem.dataset.fieldId = field.id;
+
+						let subtype = this.getSubtypeFromMime(file.type);
+						uploadItem.dataset.subtype = subtype;
+						switch (subtype) {
+							case 'image':
+								[
+									img.src,
+									img.alt
+								] = [
+									previewUrl,
+									file.name ?? upload.meta?.originalName ?? ''
+								];
+								video.remove();
+								preview.remove();
+								break;
+							case 'video':
+								video.src = previewUrl;
+								img.remove();
+								preview.remove();
+								break;
+							case 'document':
+								let extension = '';
+								let icon;
+								switch (extension) {
+									case 'pdf':
+										icon = window.getIcon('file-pdf');
+										break;
+									case 'csv':
+										icon = window.getIcon('file-csv');
+										break;
+									case 'doc':
+										icon = window.getIcon('file-doc');
+										break;
+									case 'txt':
+										icon = window.getIcon('file-txt');
+										break;
+									case 'xls':
+										icon = window.getIcon('file-xls');
+										break;
+									default:
+										icon = window.getIcon('file');
+										break;
+								}
+
+								preview.innerText = upload.originalFile.name;
+								preview.prepend(icon);
+								img.remove();
+								video.remove();
+								break;
+						}
+
+						// Store URL for cleanup later
+						uploadItem.dataset.previewUrl = previewUrl;
+					} catch (error) {
+						console.warn('Failed to create preview for upload:', upload.id, error);
+					}
+				}
+
+				// Set upload metadata
+				const nameEl = uploadItem.querySelector('summary span');
+				if (nameEl) {
+					nameEl.textContent = upload.meta?.originalName || 'Unknown file';
+				}
+
+				const metaEl = uploadItem.querySelector('details');
+				if (metaEl && upload.meta) {
+					metaEl.textContent = `${this.formatBytes(upload.meta.size)} • ${upload.meta.type}`;
+				}
+
+				// Update input IDs safely
+				uploadItem.querySelectorAll('input').forEach(input => {
+					let id = input.id;
+					if (id) {
+						let newId = id + upload.id;
+						let label = input.parentNode.querySelector(`label[for="${id}"]`);
+						input.id = newId;
+						if (label) {
+							label.htmlFor = newId;
+						}
+					}
+				});
+
+				if (itemGrid) {
+					itemGrid.appendChild(uploadItem);
+				}
+			}
+
+			notification.querySelector('.wrap').appendChild(itemGrid);
+		}
+
+		document.querySelector('.field.upload').appendChild(notification);
+		notification = document.querySelector('dialog.restore-uploads');
+		this.restoreModal = new window.jvbModal(notification);
+		this.restoreSelection = new window.jvbHandleSelection({
+			container: notification,
+			ui: {
+				selectAll: notification.querySelector('#select-all-restore'),
+				count: notification.querySelector('.selection-count'),
+			},
+		});
+
+		this.restoreModal.handleOpen();
+
+	}
+
+	async handleRestoreUploads() {
+		let notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) {
+			return;
+		}
+
+		const selectedUploads = this.getSelectedRestorationUploads(notification);
+		if (selectedUploads.length === 0) {
+			return;
+		}
+		await this.restoreSelectedUploads(selectedUploads);
+
+		this.cleanupRestore();
+	}
+
+	async handleRestoreAll() {
+		let notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) {
+			return;
+		}
+		// Gets ALL uploads from notification without checking selection
+		const allUploads = [];
+		notification.querySelectorAll('.item.upload').forEach(item => {
+			let uploadId = item.dataset.uploadId;
+			let fieldId = item.dataset.fieldId;
+			allUploads.push({ uploadId, fieldId });
+		});
+
+		await this.restoreSelectedUploads(allUploads);
+		this.cleanupRestore();
+	}
+
+	showSaveIndicator(key) {
+		// Optional: show user that state is being saved
+	}
+
+	cleanupRestore() {
+		this.restoreModal.handleClose();
+		this.restoreSelection.destroy();
+		this.restoreSelection = null;
+		this.restoreModal.destroy();
+		this.restoreModal.modal.remove();
+		this.restoreModal = null;
+	}
+
+	async cleanupStoredUploads() {
+		await this.fieldStore.clear();
+		await this.uploadStore.clear();
+	}
+
+	/*******************************************************************************
+	 * EVENT SYSTEM
+	 *******************************************************************************/
+
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data = {}) {
+		this.subscribers.forEach(cb => {
+			try {
+				cb(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
+			}
+		});
+	}
+
+	/*******************************************************************************
+	 * DESTROY & CLEANUP
+	 *******************************************************************************/
+
+	destroy() {
+		document.removeEventListener('click', this.clickHandler);
+		document.removeEventListener('change', this.changeHandler);
+		document.removeEventListener('dragenter', this.dragEnterHandler);
+		document.removeEventListener('dragleave', this.dragLeaveHandler);
+		document.removeEventListener('dragover', this.dragOverHandler);
+		document.removeEventListener('drop', this.dropHandler);
+
+		if (this.dragController) {
+			this.dragController.destroy();
+		}
+
+		this.selectionHandlers.forEach(handler => handler.destroy());
+		this.selectionHandlers.clear();
+
+		this.cleanupAllPreviewUrls();
+
+		this.sortableInstances.forEach(instance => {
+			if (instance?.destroy) instance.destroy();
+		});
+		this.sortableInstances.clear();
+
+		this.uploadElements.clear();
+		this.fieldElements.clear();
+		this.groupElements.clear();
+		this.selected.clear();
+		this.subscribers.clear();
+	}
+}
+
+// Initialize when DOM is ready
+document.addEventListener('DOMContentLoaded', async function () {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbUploads = new UploadManager();
+		}
+	});
+});

--
Gitblit v1.10.0