From 42fa8304ddb811b0f725f245130f70c0f5e86a6c Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 04 Nov 2025 06:12:02 +0000
Subject: [PATCH] =Refactored LoginManager to be more extensible and configurable, as well as an AjaxRateLimiter

---
 assets/js/concise/UploadManager.js | 4468 +++++++++++++++++++++++++++++------------------------------
 1 files changed, 2,202 insertions(+), 2,266 deletions(-)

diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index 8328177..0fd8f6e 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -4,60 +4,89 @@
 		this.queue = window.jvbQueue;
 		this.a11y = window.jvbA11y;
 		this.error = window.jvbError;
-		this.notifications = window.jvbNotifications;
 
 		//Load Datastore
-		this.initDB();
+		this.fieldStore = new window.jvbStore({
+			name: 'upload_fields',
+			storeName: 'fieldStates',
+			keyPath: 'id',
+			version: 2,
 
-		//State management
+			indexes: [
+				{ name: 'fieldId', keyPath: 'fieldId' },
+				{ name: 'timestamp', keyPath: 'timestamp' },
+				{ name: 'content', keyPath: 'content' },
+				{ name: 'itemId', keyPath: 'itemId' },
+				{ name: 'status', keyPath: 'status' }
+			],
+
+			stripDOMReferences: true,
+			TTL: 86400000*7 // 24 hours -> 1 week
+		});
+
+		this.uploadStore = new window.jvbStore({
+			name: 'uploads',
+			storeName: 'uploads',
+			keyPath: 'id',
+			storeBlobs: true,
+
+			indexes: [
+				{ name: 'fieldId', keyPath: 'fieldId' },
+				{ name: 'status', keyPath: 'status' },
+				{ name: 'groupId', keyPath: 'groupId' },
+				{ name: 'attachmentId', keyPath: 'attachmentId' }
+			],
+		});
+
+		window.jvbUploadBlobs = this.uploadStore;
+
+		// Subscribe to store events
+		this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
+		this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
+
+		//Load Worker
+		this.initWorker();
+
+		// Core data structures
 		this.fields = new Map();
 		this.uploads = new Map();
-		this.uploadBlobs = new Map();
-		this.timeouts = new Map();
-		this.selected = new Map();
-
-		//Worker
-		this.worker = {
-			worker: null,
-			timeout: null,
-			tasks: new Map(),
-			restart: {
-				count: 0,
-				max: 3,
-			},
-			settings: {
-				timeout: 10000, //10 seconds per image
-				batchSize: 1,
-				maxConcurrent: 3,
-				restartAfterTimeout: true
-			}
-		};
-
-		//Groups!
-		this.touch = {
-			x: null,
-			y: null
-		}
-		this.hasBulkContext = document.querySelector('details.uploader')!==null;
-		this.isTouching = false;
 		this.groups = new Map();
-
+		this.selected = new Map();
+		this.selectionHandlers = new Map();
+		this.previewUrls = new Set();
 		//Notification and Subscribers
 		this.subscribers = new Set();
 
-		this.settings = {
-			allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'],
-			maxFileSize: 5242880,
-			maxProcessingTime: 120000, // 2 minutes max for processing
-			processingCheckInterval: 5000, // Check every 5 seconds
-			smartCompression: true,
-			fieldTypes: {
-				'single': { maxFiles: 1, allowMultiple: false },
-				'gallery': { maxFiles: 20, allowMultiple: true },
-				'groupable': { maxFiles: 20, allowMultiple: true }
+		// Controllers (will be initialized based on features)
+		this.dragController = null;
+
+		// Selectors
+		this.selectors = {
+			field: {
+				field: '[data-upload-field]',
+				input: 'input[type="file"]',
+				hiddenValue: 'input[type="hidden"]',
+				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...',
@@ -74,192 +103,421 @@
 	}
 
 	async init() {
-		this.initElements();
+		// Load existing data
+		await this.loadFields();
+		await this.loadUploads();
+		// Initialize fields
+		this.initializeFields();
+
+		// Set up core listeners
 		this.initListeners();
-		this.initCompressionWorker();
+
 		this.queue.subscribe((event, operation) => {
-			console.log('Operation Endpoint: ', operation.endpoint);
-			if (operation.endpoint !== 'uploads') {
+			if (operation.endpoint !== 'uploads' && operation.endpoint !== 'uploads/meta') {
 				return;
 			}
+			const fieldId = operation.data instanceof FormData
+				? operation.data.get('fieldId')
+				: operation.data.fieldId;
 			switch(event) {
 				case 'cancel-operation':
-					this.clearField(operation.data.get('field_key'));
+					if (fieldId) {
+						this.clearField(fieldId);
+					}
 					break;
 				case 'operation-status':
-					console.log('Operation Data: ',operation.data);
-					const fieldId = operation.data?.field_key ||
-						(operation.data instanceof FormData ?
-							operation.data.get('field_key') : null);
-
 					if (fieldId) {
-						console.log('Updating field status:', fieldId, operation.status);
 						this.updateFieldStatus(fieldId, operation.status);
 					}
 					break;
+				case 'operation-complete':
+					const results = operation.result?.data || [];
+					results.forEach(result => {
+						const upload = this.uploads.get(result.upload_id);
+						if (upload) {
+							upload.attachmentId = result.attachment_id;
+							upload.status = 'completed';
+							this.uploads.set(upload.id, upload);
+						}
+					});
+					if (fieldId) {
+						this.cleanField(fieldId);
+					}
+					break;
 			}
+
 		});
-		await this.checkPendingUploads();
-		this.scanFields();
+
+		window.addEventListener('beforeunload', () => {
+			this.cleanupAllPreviewUrls();
+		});
 	}
 
-	initElements() {
-		this.selectors = {
-			field: {
-				field: '.field.image',
-				dropZone: '.file-upload-container',
-				preview: '.item-grid.preview',
-				hiddenValue: 'input[type="hidden"]',
-				progress: {
-					progress: '.progress',
-					details: '.progress .details',
-					fill: '.progress .fill',
-					count: '.progress .count'
-				},
+	initWorker() {
+		this.worker = {
+			worker: null,
+			timeout: null,
+			tasks: new Map(),
+			restart: {
+				count: 0,
+				max: 3,
 			},
-			item: {
-				img: 'img',
-				progress: {
-					progress: '.progress',
-					details: '.progress .details',
-					fill: '.progress .fill',
-					count: '.progress .count'
-				},
-				status: '.status',
-				select: '[name*="select-item"]',
-				actions: '.item-actions',
-				featured: '[name="featured"]',
-				meta: '.upload-meta'
-			},
-			groups: {
-				container: '.item-grid.groups',
-				display: '.group-display',
-				selectAll: '#select-all-uploads',
-				actions: '.selection-actions',
-				info: '.selection-controls .info',
-				count: '.selection-count',
-				group: '.upload-group',
-				empty: '.empty-group'
+			settings: {
+				timeout: 10000, //10 seconds per image
+				batchSize: 1,
+				maxConcurrent: 3,
+				restartAfterTimeout: true
 			}
 		};
-		this.ui = {};
 	}
 
-	scanFields() {
-		document.querySelectorAll(this.selectors.field.field).forEach(uploader => {
+	/**
+	 * Initialize all upload fields on the page
+	 */
+	initializeFields() {
+		const fields = document.querySelectorAll(this.selectors.field.field);
+		fields.forEach(uploader => {
 			this.registerUploader(uploader);
 		});
 	}
 
-	/**
-	 *
-	 * @param {HTMLElement} uploader
-	 * @param {object} options
-	 * @param {string} options.id Uploader field ID: defaults to uploader.dataset.fieldId
-	 * @param {string} options.type Uploader type: defaults to uploader.dataset.type
-	 * @param {number} options.maxFiles Maximum files to allow: defaults to type defaults
-	 * @param {boolean} options.multiple Whether to allow multiple uploads
-	 * @param {number} options.itemID The post or term ID this is for.
-	 * @param {string} options.mode
-	 * @returns {string}
-	 */
-	registerUploader(uploader, options = {}) {
-		//Determine if this is for a post, term, content uploader, or option
-		let key = uploader.dataset['uploader']??this.determineKey(uploader);
+	scanFields(container) {
+		const fields = container.querySelectorAll(this.selectors.field.field);
+		fields.forEach(uploader => {
+			this.registerUploader(uploader);
+		});
+	}
 
-		uploader.dataset['uploader'] = key;
+	registerUploader(uploader) {
+		const fieldId = this.determineFieldId(uploader);
+		const config = this.extractFieldConfig(uploader);
 
-		if (!this.fields.has(key)) {
-			let type = uploader.dataset.type;
+		// Create field data structure
+		const field = {
+			id: fieldId,
+			config: config,
+			element: uploader,
+			ui: this.buildFieldUI(uploader),
+			uploads: new Set(),
+			groups: new Set(),
+			state: 'ready',
+		};
 
-			let typeConfig = this.settings.fieldTypes[type]??this.settings.fieldTypes['single'];
-			let config = {
-				key: key,
-				name: uploader.dataset.field,
-				ui: {},
-				type: type,
-				maxFiles: typeConfig.maxFiles,
-				multiple: typeConfig.allowMultiple,
-				content: uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??false,
-				itemID: uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??false,
-				context: uploader.dataset.context??uploader.closest('dialog')?.dataset.context??false,
-				mode: uploader.dataset.mode??'direct',
-				... options
-			};
+		this.fields.set(fieldId, field);
+		uploader.dataset.uploader = fieldId;
+		this.addFieldSelectionHandler(fieldId);
 
-			config.ui = window.uiFromSelectors(this.selectors, uploader);
-			config.ui.groups.groups = new Map();
-
-			this.selected.set(key, new Set());
-			this.fields.set(key, config);
-			if(config.type === 'groupable' && !this.hasGroups) {
-				this.initGroupListeners();
-			}
+		if (config.destination === 'post_group' && !this.dragController) {
+			this.initGroupFeatures();
 		}
-		return key;
+
+		return fieldId;
 	}
 
 	/**
-	 * Builds a key from the uploader, built from the Content Type, ItemID, and FieldName
-	 * @param uploader
-	 * @returns {string}
+	 * Extract configuration from field element
 	 */
-	determineKey(uploader) {
-		let content = uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??'';
-		let itemID = uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??'';
-		let field = uploader.dataset.field;
-		return `${content}_${itemID}_${field}`;
+	extractFieldConfig(fieldElement) {
+		return {
+			destination: fieldElement.dataset.destination || 'meta',
+			content: fieldElement.dataset.content || null,
+			mode: fieldElement.dataset.mode || 'direct',
+			type: fieldElement.dataset.type || 'single',
+			name: fieldElement.dataset.field,  // Field name for meta
+			itemID: fieldElement.dataset.itemId || 0,  // Post/term/user ID
+			maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
+			subtype: fieldElement.dataset.subtype || 'image'
+		};
 	}
 
 	/**
-	 *
-	 * @param {HTMLElement} element
+	 * Build UI element references for a field
 	 */
-	getFieldIdFromElement(element) {
-		let field = element.closest('.field.image');
-		if (!field) {
+	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;
+	}
+
+	/**
+	 * Set up core event listeners
+	 */
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+
+		// External file drops
+		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);
+	}
+
+	/**
+	 * Initialize group-specific features (drag & drop for rearranging)
+	 */
+	initGroupFeatures() {
+		// Initialize drag controller for rearranging items
+		this.dragController = new window.jvbDragHandler({
+			// What can be dragged
+			draggableSelector: this.selectors.items.item,
+
+			// Where items can be dropped
+			dropTargetSelector: `${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,
+
+			// Don't start drag on interactive elements
+			ignoreSelector: 'input:not(.upload-select), button, select, textarea, details, summary, a',
+			previewElement: 'img, video, .icon',
+
+			// Extract upload ID from element
+			getItemId: (element) => {
+				return element.dataset.uploadId;
+			},
+
+			// Get selected items for multi-drag
+			getSelectedItems: (element) => {
+				const fieldId = this.getFieldIdFromElement(element);
+				const uploadId = element.dataset.uploadId;
+				const selected = this.getCurrentSelection(fieldId);
+
+				if (selected && selected.includes(uploadId)) {
+					return selected;
+				}
+
+				return [uploadId];
+			},
+
+			// Validate drop location
+			validateDrop: (itemIds, targetElement) => {
+				const targetFieldId = this.getFieldIdFromElement(targetElement);
+				const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
+				const itemFieldId = this.getFieldIdFromElement(itemElement);
+
+				return targetFieldId === itemFieldId;
+			},
+
+			// Handle successful drop
+			onDrop: (itemIds, targetElement) => {
+				this.handleItemDrop(itemIds, targetElement);
+				targetElement.scrollIntoView({behavior:'smooth', block:'center'});
+			},
+
+			// Optional callbacks
+			onDragStart: (itemIds) => {
+			},
+
+			onDragEnd: (itemIds, success) => {
+				if (success) {
+					// Clear selection after successful move
+					const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
+					const fieldId = this.getFieldIdFromElement(itemElement);
+					const handler = this.selectionHandlers.get(fieldId);
+					handler?.clearSelection();
+				}
+			},
+
+			// Preview options
+			previewOptions: {
+				multiOffset: { x: -60, y: -80 },
+				singleOffset: { x: -50, y: -60 },
+				showCount: true
+			}
+		});
+	}
+
+	/*******************************************************************************
+	 * EXTERNAL FILE DROP HANDLERS (for new uploads from desktop)
+	 *******************************************************************************/
+
+	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;
 		}
-		return field.dataset.uploader??this.determineKey(field);
+
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+
+		if (dropZone) {
+			e.preventDefault();
+			dropZone.classList.add('dragover');
+		}
 	}
 
-	getFieldFromElement(element) {
-		let id = this.getFieldIdFromElement(element);
-		return (this.fields.has(id)) ? this.fields.get(id) : false;
+	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';
+		}
 	}
 
-	getUploadFromElement(element) {
-		let id = this.getUploadIdFromElement(element);
-		return (this.uploads.has(id)) ? this.uploads.get(id) : false;
+	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`);
+		} else {
+			console.error('No field ID found for drop zone');
+		}
 	}
 
-	getUploadIdFromElement(element) {
-		let upload = element.closest('[data-upload-id]');
-		return upload?.dataset.uploadId || null;
-	}
+	/*******************************************************************************
+	 * ITEM DROP HANDLER (for rearranging existing uploads)
+	 *******************************************************************************/
 
-	getGroupFromElement(element) {
-		let groupId = this.getGroupIdFromElement(element);
-		return (this.groups.has(groupId)) ? this.groups.get(groupId) : false;
-	}
-	getGroupIdFromElement(element) {
-		return element.dataset.groupId??element.closest('[data-group-id]')?.dataset.groupId??element.closest(':has([data-group-id])')?.querySelector('[data-group-id]')?.dataset.groupId??null;
-	}
+	/**
+	 * Handle items being dropped (called by DragController)
+	 */
+	handleItemDrop(itemIds, targetElement) {
+		const isPreviewDrop = targetElement.classList.contains('preview');
+		let actualTarget = targetElement;
 
-	getModalType(field) {
-		// Safety check for field.ui
-		if (!field || !field.ui || !field.ui.field || !field.ui.field.field) {
-			return null;
+		// Handle drop on empty group placeholder
+		if (targetElement.classList.contains('empty-group')) {
+			const fieldId = this.getFieldIdFromElement(targetElement);
+			const group = this.createGroup(fieldId);
+
+			if (!group) {
+				console.error('Failed to create group');
+				return;
+			}
+
+			actualTarget = group.grid;
 		}
 
-		const dialog = field.ui.field.field.closest('dialog');
-		if (!dialog) return null;
+		// Move each item to target
+		itemIds.forEach(uploadId => {
+			if (isPreviewDrop) {
+				// Moving back to preview (ungrouping)
+				this.removeFromGroup(uploadId);
+			} else {
+				// Moving to a group
+				this.addToGroup(uploadId, actualTarget);
+			}
+		});
 
-		if (dialog.classList.contains('edit')) return 'edit';
-		if (dialog.classList.contains('create')) return 'create';
-		if (dialog.classList.contains('bulkEdit')) return 'bulkEdit';
+		// Persist state
+		const fieldId = this.getFieldIdFromElement(targetElement);
+		this.schedulePersistance(fieldId);
 
-		return dialog.className;
+		// Announce for accessibility
+		const message = itemIds.length > 1
+			? `Moved ${itemIds.length} items`
+			: 'Moved item';
+		this.a11y.announce(message);
+	}
+
+	/*******************************************************************************
+	 * CLICK HANDLERS
+	 *******************************************************************************/
+
+	handleClick(e) {
+		// File input triggers
+		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 fieldId = this.getFieldIdFromElement(e.target);
+			const files = Array.from(e.target.files);
+
+			if (files.length > 0 && fieldId) {
+				this.processFiles(fieldId, files);
+			}
+		}
+
+		// Meta field changes
+		if (fieldId) {
+			if (this.fields.get(fieldId).config.destination === 'post_group') {
+				this.handleGroupMetaChange(e.target);
+			} else {
+				this.queueUploadMeta(e);
+			}
+		}
+	}
+
+	/********************************************************************************
+	 UTILITY
+	********************************************************************************/
+	getCurrentSelection(fieldId) {
+		let selected = [];
+		for (let [key, handler] of this.selectionHandlers) {
+			if ((fieldId === key || key.includes(fieldId)) && handler.selectedItems.size > 0) {
+				selected = selected.concat([... handler.selectedItems]);
+			}
+		}
+		return selected;
+	}
+
+	getSubtypeFromMime(mimeType) {
+		if (mimeType.startsWith('image/')) return 'image';
+		if (mimeType.startsWith('video/')) return 'video';
+		return 'document';
 	}
 
 	getStatusText(status) {
@@ -270,7 +528,6 @@
 		return window.getIcon(this.queue.icons[status]);
 	}
 	getStatusProgress(status) {
-		console.log('Getting status progress for: ', status);
 		switch (status) {
 			case 'local_processing':
 				return 28;
@@ -289,1133 +546,518 @@
 		}
 	}
 
-	/******************************************************************************
-	 LISTENERS
-	******************************************************************************/
-	initListeners() {
-		this.clickHandler 		= this.handleClick.bind(this);
-		this.changeHandler 		= this.handleChange.bind(this);
-
-		if (this.hasBulkContext) {
-			this.pasteHandler 		= this.handlePaste.bind(this);
-			document.addEventListener('paste', this.pasteHandler);
+	getModalType(field) {
+		// Return cached value if available
+		if (field._cachedModalType !== undefined) {
+			return field._cachedModalType;
 		}
 
-
-		document.addEventListener('click', this.clickHandler);
-		document.addEventListener('change', this.changeHandler);
-		window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
-	}
-	clearListeners() {
-		document.removeEventListener('click', this.clickHandler);
-		document.removeEventListener('change', this.changeHandler);
-		if (this.hasBulkContext) {
-			document.removeEventListener('paste', this.pasteHandler);
-		}
-	}
-
-	initGroupListeners() {
-		this.hasGroups = true;
-
-		this.dragStartHandler 	= this.handleDragStart.bind(this);
-		this.dragEndHandler 	= this.handleDragEnd.bind(this);
-		this.dragEnterHandler 	= this.handleDragEnter.bind(this);
-		this.dragOverHandler 	= this.handleDragOver.bind(this);
-		this.dragLeaveHandler 	= this.handleDragLeave.bind(this);
-		this.dropHandler 		= this.handleDrop.bind(this);
-
-		this.touchStartHandler 	= this.handleTouchStart.bind(this);
-		this.touchMoveHandler 	= this.handleTouchMove.bind(this);
-		this.touchEndHandler 	= this.handleTouchEnd.bind(this);
-		this.touchCancelHandler	= this.handleTouchCancel.bind(this);
-
-		document.addEventListener('dragstart', this.dragStartHandler);
-		document.addEventListener('dragend', this.dragEndHandler);
-		document.addEventListener('dragenter', this.dragEnterHandler);
-		document.addEventListener('dragover', this.dragOverHandler);
-		document.addEventListener('dragleave', this.dragLeaveHandler);
-		document.addEventListener('drop', this.dropHandler);
-
-		document.addEventListener('touchstart', this.touchStartHandler);
-		document.addEventListener('touchmove', this.touchMoveHandler);
-		document.addEventListener('touchend', this.touchEndHandler);
-		document.addEventListener('touchcancel', this.touchCancelHandler);
-	}
-	clearGroupListeners() {
-		document.removeEventListener('dragstart', this.dragStartHandler);
-		document.removeEventListener('dragend', this.dragEndHandler);
-		document.removeEventListener('dragenter', this.dragEnterHandler);
-		document.removeEventListener('dragover', this.dragOverHandler);
-		document.removeEventListener('dragleave', this.dragLeaveHandler);
-		document.removeEventListener('drop', this.dropHandler);
-
-		document.removeEventListener('touchstart', this.touchStartHandler);
-		document.removeEventListener('touchmove', this.touchMoveHandler);
-		document.removeEventListener('touchend', this.touchEndHandler);
-		document.removeEventListener('touchcancel', this.touchCancelHandler);
-	}
-
-	handleClick(e) {
-		if (!e.target.closest(this.selectors.field.field)) {
-			return;
+		// Safety check for field.element
+		if (!field || !field.element) {
+			field._cachedModalType = null;
+			return null;
 		}
 
-		if (window.targetCheck(e, '.restart-uploads')) {
-			e.preventDefault();
-			const fieldId = this.getFieldIdFromElement(e.target);
-			this.restartUploads(fieldId);
-		} else if (window.targetCheck(e, '.dismiss-cache-restore')) {
-			e.preventDefault();
-			const notification = e.target.closest('.upload-recovery-notification');
-			if (notification) notification.remove();
-		} else if (window.targetCheck(e, '#select-all-uploads')) {
-			e.preventDefault();
-			this.handleSelectAll(e.target);
-		} else if (window.targetCheck(e, '.upload-select')) {
-			const isShiftClick = e.shiftKey && this.lastClickedUpload;
-			if (isShiftClick) {
-				e.preventDefault();
-				this.handleRangeSelection(e.target, e);
-			} else {
-				this.updateSelection(e);
-			}
-		} else if (window.targetCheck(e, '.create-from-selection')) {
-			e.preventDefault();
-			let group = this.createGroup(this.getFieldFromElement(e.target));
-			this.addSelectionToGroup(group);
-		} else if (window.targetCheck(e, '.remove-selection')) {
-			e.preventDefault();
-			this.removeSelection(e.target);
-		} else if (window.targetCheck(e, '.add-to-group, .add-selection-to-group')) {
-			e.preventDefault();
-			this.addSelectionToGroup(e.target);
-		} else if (window.targetCheck(e, '.remove-group')) {
-			e.preventDefault();
-			const groupElement = e.target.closest('.upload-group');
-			if (groupElement) {
-				let field = this.getFieldFromElement(groupElement);
-				this.removeGroup(groupElement, true);
-			}
-		} else if (window.targetCheck(e, '.remove')) {
-			e.preventDefault();
-			const uploadId = this.getUploadIdFromElement(e.target);
-			const fieldId = this.getFieldIdFromElement(e.target);
-			if (uploadId && fieldId) {
-				this.removeUpload(fieldId, uploadId);
-			}
-		} else if (window.targetCheck(e, '.submit-uploads')) {
-			e.preventDefault();
-			const fieldId = this.getFieldIdFromElement(e.target);
-			this.submitUploads(fieldId);
-		} else if (window.targetCheck(e, '.retry-upload')) {
-			e.preventDefault();
-			const uploadId = this.getUploadIdFromElement(e.target);
-			this.retryUpload(uploadId);
-		}
-	}
-	handleChange(e) {
-		if (!e.target.closest(this.selectors.field.field) || e.target.classList.contains(this.selectors.field.hiddenValue)) {
-			return;
-		}
-		e.preventDefault();
-
-		if (window.targetCheck(e, '[type="file"]')) {
-			console.log(this.fields);
-			let field = this.getFieldFromElement(e.target);
-			console.log(field);
-			if (!field) {
-				console.warn('File change on unregistered field: ', field.key)
-				return;
-			}
-
-			const files = Array.from(e.target.files);
-			if (files.length === 0) return;
-
-			this.processFiles(field.key, files);
-			e.target.value = '';
-		} else if (e.target.name.includes('select-')) {
-			this.updateSelection(e);
-		} else if (e.target.closest('.upload-meta')) {
-			e.preventDefault();
-			let name = e.target.name;
-			let value = e.target.value;
-			let upload = this.getUploadFromElement(e.target);
-			upload.changes[name] = value;
-			this.uploads.set(upload.id, upload);
-			this.persistFieldState(upload.fieldId);
-
-			//It's meta!
-			//TODO:
-			//Step 1) determine whether the images have already been sent to the server. If not, we must wait until they have been
-			//Step 2) Queue the Meta changes. No need to wait, the Queue.js will handle any debouncing/timeouts
-			//Ensure the dependencies have all operations stored to the field that the images were uploaded with (can be multiple)
-			//Send to server for processing
-		} else if (e.target.closest('.group.fields')) {
-			let group = this.getGroupFromElement(e.target);
-			let name = e.target.name;
-			group.changes[name] = e.target.value;
-
-			this.persistFieldState(group.fieldId);
-			this.groups.set(group.id, group);
-		}
-	}
-
-	handlePaste(e) {
-		window.debouncer.schedule(
-			'imagePaste',
-			() => {
-				const items = Array.from(e.clipboardData.items);
-				const imageItems = items.filter(item => item.type.startsWith('image/'));
-
-				if (imageItems.length === 0) return;
-
-				e.preventDefault();
-
-				const fieldId = this.getFieldIdFromElement(e.target);
-				if (!fieldId) return;
-
-				// Convert clipboard items to files
-				const files = [];
-				imageItems.forEach((item, index) => {
-					const file = item.getAsFile();
-					if (file) {
-						// Rename for clarity
-						const newFile = new File([file], `pasted_image_${index + 1}.png`, {
-							type: file.type,
-							lastModified: Date.now()
-						});
-						files.push(newFile);
-					}
-				});
-
-				if (files.length > 0) {
-					this.processFiles(fieldId, files);
-				}
-			},
-			100
-		);
-	}
-
-	isTouchOnFormElement(target) {
-		// Check if target is a form element or inside one
-		const formElements = [
-			'input', 'button', 'label', 'select', 'textarea',
-		];
-
-		return formElements.some(selector => {
-			return target.matches(selector) || target.closest(selector);
-		});
-	}
-	/**** DRAG AND TOUCH *****/
-	startDragOperation(config) {
-		const {
-			primaryElement,
-			sourceType,
-			startPosition,
-			event
-		} = config;
-
-		const uploadId = this.getUploadIdFromElement(primaryElement);
-		const fieldId = this.getFieldIdFromElement(primaryElement);
-
-		// Determine what items to drag
-		const draggedItems = this.getDraggedItems(primaryElement);
-
-		// Initialize drag state
-		this.dragState = {
-			primaryItem: uploadId,
-			draggedItems: draggedItems,
-			isDragging: true,
-			isMultiDrag: draggedItems.length > 1,
-			fieldId: fieldId,
-			sourceType: sourceType,
-			startTime: Date.now(),
-			startPosition: startPosition,
-			currentPosition: startPosition,
-			currentTarget: null,
-			validTarget: null,
-			dragPreview: null,
-			touchId: sourceType === 'touch' ? event.touches[0]?.identifier : null,
-			touchMoved: false
-		};
-
-		// Create drag preview
-		this.createDragPreview(primaryElement);
-
-		// Apply dragging state
-		this.applyDraggingState(true);
-
-		const announceText = this.dragState.isMultiDrag
-			? `Started dragging ${draggedItems.length} items`
-			: 'Started dragging item';
-
-		this.a11y.announce(announceText);
-		this.provideDragFeedback('start');
-
-		return true;
-	}
-
-	updateDragOperation(position, elementUnderPointer) {
-		if (!this.dragState.isDragging) return;
-
-		const { sourceType, startPosition } = this.dragState;
-
-		// Update position
-		this.dragState.currentPosition = position;
-
-		// Check for significant movement (touch)
-		if (sourceType === 'touch' && !this.dragState.touchMoved) {
-			const deltaX = Math.abs(position.x - startPosition.x);
-			const deltaY = Math.abs(position.y - startPosition.y);
-
-			if (deltaX > 10 || deltaY > 10) {
-				this.dragState.touchMoved = true;
-			}
+		const dialog = field.element.closest('dialog');
+		if (!dialog) {
+			field._cachedModalType = null;
+			return null;
 		}
 
-		// Update preview and target
-		this.updateDragPreview(position);
-		this.updateDropTarget(elementUnderPointer);
-	}
+		let modalType = null;
+		if (dialog.classList.contains('edit')) modalType = 'edit';
+		else if (dialog.classList.contains('create')) modalType = 'create';
+		else if (dialog.classList.contains('bulkEdit')) modalType = 'bulkEdit';
+		else modalType = dialog.className;
 
-	endDragOperation(elementUnderPointer = null) {
-		if (!this.dragState.isDragging) return;
-
-		const wasSuccessful = (this.dragState.sourceType === 'drag' || this.dragState.touchMoved) &&
-			this.dragState.validTarget;
-
-		// Process drop if valid - but only here, not in handleDrop
-		if (wasSuccessful && this.dragState.validTarget) {
-			this.processItemDrop({
-				itemIds: this.dragState.draggedItems,
-				targetElement: this.dragState.validTarget,
-				fieldId: this.dragState.fieldId,
-				dropType: this.dragState.isMultiDrag ? 'multiple' : 'single',
-				sourceType: this.dragState.sourceType
-			});
-		}
-
-		// Cleanup
-		this.cleanupDragOperation();
-
-		const announceText = wasSuccessful
-			? (this.dragState.isMultiDrag ? `Moved ${this.dragState.draggedItems.length} items` : 'Item moved')
-			: 'Drag cancelled';
-
-		this.a11y.announce(announceText);
-	}
-
-	/**
-	 * Shared method to process any drop operation (drag or touch)
-	 * @param {Object} dropData - Standardized drop data
-	 * @returns {boolean} Success status
-	 */
-	processItemDrop(dropData) {
-		const {
-			itemIds,
-			targetElement,
-			fieldId,
-			dropType,
-			sourceType
-		} = dropData;
-
-		if (!itemIds?.length || !targetElement || !fieldId) {
-			return false;
-		}
-
-		// Determine if it's a preview drop
-		let isPreviewDrop = targetElement.classList.contains('item-grid') && targetElement.classList.contains('preview');
-
-		// Handle empty group drops by creating the group element
-		let actualTarget = targetElement;
-		if (targetElement.classList.contains('empty-group')) {
-			let group = this.createGroup(fieldId);
-			actualTarget = group.querySelector('.item-grid');
-			isPreviewDrop = false;
-		}
-
-		// Use existing addImageToGroup method for each item
-		// This method already handles:
-		// - removeImageFromCurrentLocation (cleanup of old location)
-		// - Adding to new location
-		// - Updating field.posts data structure
-		// - Caching the data
-		itemIds.forEach(uploadId => {
-			this.addImageToGroup(uploadId, actualTarget, isPreviewDrop);
-		});
-
-		// Clear selections for multi-drops
-		if (dropType === 'multiple') {
-			const field = this.fields.get(fieldId);
-			this.clearAllSelections(field);
-		}
-
-		// Announce completion
-		const announceText = dropType === 'multiple'
-			? `Moved ${itemIds.length} images to ${isPreviewDrop ? 'main area' : 'group'}`
-			: `Image moved to ${isPreviewDrop ? 'main area' : 'group'}`;
-
-		this.a11y.announce(announceText);
-		this.provideFeedback(sourceType, 'success', {
-			count: itemIds.length,
-			isMultiple: dropType === 'multiple'
-		});
-
-		return true;
-	}
-
-	clearAllSelections(field) {
-		// Clear all selection checkboxes in the entire field container
-		const allCheckboxes = field.container.querySelectorAll('[name*="select-item"]');
-		allCheckboxes.forEach(checkbox => {
-			checkbox.checked = false;
-		});
-
-		// Update the select all state
-		if (field.selectAll) {
-			field.selectAll.checked = false;
-			const label = field.selectAll.nextElementSibling;
-			if (label) {
-				label.textContent = 'Select All';
-			}
-		}
-
-		// Hide selection controls
-		if (field.selectActions) field.selectActions.hidden = true;
-		if (field.selectInfo) field.selectInfo.hidden = true;
-	}
-
-	cleanupDragOperation() {
-		if (this.dragState.dragPreview) {
-			this.dragState.dragPreview.remove();
-		}
-
-		this.applyDraggingState(false);
-		this.clearDropTargetStates();
-
-		// Reset state
-		this.dragState.isDragging = false;
-		this.dragState.dragPreview = null;
-		this.dragState.draggedItems = [];
-	}
-
-	/**
-	 * Determine what items to drag (single or multiple selection)
-	 */
-	getDraggedItems(element) {
-		const selectedUploads = this.getSelectedUploads(element);
-		const primaryUploadId = element.dataset.uploadId;
-
-		// If we have multiple selections and primary is selected, drag all
-		if (selectedUploads.length > 1 && selectedUploads.includes(primaryUploadId)) {
-			return selectedUploads;
-		}
-
-		// Otherwise, just drag the primary item
-		return [primaryUploadId];
-	}
-
-	/**
-	 * Apply/remove dragging visual state to items
-	 */
-	applyDraggingState(isDragging) {
-		this.dragState.draggedItems.forEach(uploadId => {
-			const element = document.querySelector(`[data-upload-id="${uploadId}"]`);
-			if (element) {
-				element.classList.toggle('dragging', isDragging);
-			}
-		});
-	}
-
-	/**
-	 * Create drag preview element
-	 */
-	createDragPreview(originalElement) {
-		const { isMultiDrag, draggedItems } = this.dragState;
-
-		if (isMultiDrag) {
-			this.dragState.dragPreview = this.createMultiDragPreview(originalElement, draggedItems);
-		} else {
-			this.dragState.dragPreview = this.createSingleDragPreview(originalElement);
-		}
-
-		this.updateDragPreview(this.dragState.startPosition);
-		document.body.appendChild(this.dragState.dragPreview);
-	}
-
-	/**
-	 * Create single item drag preview
-	 */
-	createSingleDragPreview(originalElement) {
-		const preview = originalElement.cloneNode(true);
-		preview.dataset.uploadId = preview.dataset.uploadId+'-dragging';
-		this.styleDragPreview(preview, false);
-		return preview;
-	}
-
-	styleDragPreview(preview, isMulti = false) {
-		preview.style.cssText = `
-        position: fixed;
-        z-index: 10000;
-        pointer-events: none;
-        opacity: 0.9;
-        transform: scale(1.05);
-        transition: transform 0.2s ease;
-        ${isMulti ? `
-            width: 120px;
-            height: 120px;
-            background: white;
-            border-radius: 8px;
-            box-shadow: 0 8px 32px rgba(0,0,0,0.3);
-            padding: 4px;
-        ` : `
-            border-radius: 4px;
-            box-shadow: 0 4px 16px rgba(0,0,0,0.2);
-        `}
-    `;
-
-		// Add dragging class for additional styling
-		preview.classList.add('drag-preview', 'is-dragging');
-		if (isMulti) {
-			preview.classList.add('multi-item');
-		}
-	}
-
-	/**
-	 * Create multiple items drag preview
-	 */
-	createMultiDragPreview(originalElement, draggedItems) {
-		const container = document.createElement('div');
-		container.className = 'drag-preview multi-item';
-
-		// Create stacked effect with up to 3 items
-		const displayCount = Math.min(draggedItems.length, 3);
-
-		for (let i = 0; i < displayCount; i++) {
-			const uploadId = draggedItems[i];
-			const uploadElement = document.querySelector(`[data-upload-id="${uploadId}"]`);
-
-			if (uploadElement) {
-				const stackedItem = uploadElement.cloneNode(true);
-				stackedItem.dataset.uploadId = uploadId + '_dragging';
-
-				stackedItem.style.cssText = `
-				position: absolute;
-				top: ${i * 4}px;
-				left: ${i * 4}px;
-				width: calc(100% - ${i * 4}px);
-				height: calc(100% - ${i * 4}px);
-				opacity: ${1 - (i * 0.15)};
-				transform: rotate(${(i - 1) * 2}deg);
-				z-index: ${10 - i};
-				border-radius: 4px;
-				overflow: hidden;
-			`;
-				container.appendChild(stackedItem);
-			}
-		}
-
-		// Add count badge
-		if (draggedItems.length > 1) {
-			const badge = this.createCountBadge(draggedItems.length);
-			container.appendChild(badge);
-		}
-
-		this.styleDragPreview(container, true);
-		return container;
-	}
-	/**
-	 * Update drag preview position
-	 */
-	updateDragPreview(position) {
-		if (!this.dragState.dragPreview) return;
-
-		// Calculate offset based on preview type and source
-		let offset;
-		if (this.dragState.sourceType === 'touch') {
-			offset = this.dragState.isMultiDrag ? { x: -60, y: -80 } : { x: -50, y: -60 };
-		} else {
-			offset = this.dragState.isMultiDrag ? { x: 15, y: 15 } : { x: 10, y: 10 };
-		}
-
-		const deltaX = position.x - this.dragState.startPosition.x;
-		const deltaY = position.y - this.dragState.startPosition.y;
-
-		this.dragState.dragPreview.style.transform = `translate(${deltaX + offset.x}px, ${deltaY + offset.y}px) scale(1.05)`;
-	}
-
-	/**
-	 * Update drop target highlighting
-	 */
-	updateDropTarget(elementUnderPointer) {
-		// Clear previous target
-		if (this.dragState.currentTarget) {
-			this.clearDropTargetState(this.dragState.currentTarget);
-		}
-
-		// Find valid drop target
-		const validTarget = this.findValidDropTarget(elementUnderPointer);
-
-		// Update state
-		this.dragState.currentTarget = elementUnderPointer;
-		this.dragState.validTarget = validTarget;
-
-		// Apply visual feedback
-		if (validTarget) {
-			this.applyDropTargetState(validTarget);
-
-			// Haptic feedback for touch
-			if (this.dragState.sourceType === 'touch' && navigator.vibrate) {
-				const pattern = this.dragState.isMultiDrag ? [25, 10, 25] : [25];
-				navigator.vibrate(pattern);
-			}
-		}
-	}
-
-	/**
-	 * Find valid drop target from element
-	 */
-	findValidDropTarget(element) {
-		if (!element) return null;
-
-		const postContainer = element.closest('.item-grid.group, .empty-group, .item-grid.preview');
-		if (postContainer) {
-			const fieldId = this.getFieldIdFromElement(postContainer);
-			if (fieldId === this.dragState.fieldId) {
-				return postContainer;
-			}
-		}
-
-		return null;
-	}
-
-	/**
-	 * Apply drop target visual state
-	 */
-	applyDropTargetState(target) {
-		target.classList.add('dragover');
-
-		if (this.dragState.isMultiDrag) {
-			target.classList.add('multi-drop');
-			target.setAttribute('data-item-count', this.dragState.draggedItems.length);
-		}
-	}
-
-	/**
-	 * Clear drop target state from element
-	 */
-	clearDropTargetState(target) {
-		target.classList.remove('dragover', 'multi-drop');
-		target.removeAttribute('data-item-count');
-	}
-
-	/**
-	 * Clear all drop target states
-	 */
-	clearDropTargetStates() {
-		document.querySelectorAll('.dragover').forEach(el => {
-			el.classList.remove('dragover', 'multi-drop');
-			el.removeAttribute('data-item-count');
-		});
-	}
-
-	/**
-	 * Create count badge for multi-item preview
-	 */
-	createCountBadge(count) {
-		const badge = document.createElement('div');
-		badge.className = 'selection-count-badge';
-		badge.textContent = count.toString();
-		badge.style.cssText = `
-			position: absolute;
-			top: -8px;
-			right: -8px;
-			background: var(--accent-primary);
-			color: white;
-			border-radius: 50%;
-			width: 24px;
-			height: 24px;
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			font-size: 12px;
-			font-weight: bold;
-			box-shadow: 0 2px 8px rgba(0,0,0,0.3);
-			z-index: 20;
-		`;
-		return badge;
-	}
-
-	/**
-	 * Provide feedback for drag operations
-	 */
-	provideDragFeedback(type) {
-		const hapticPatterns = {
-			start: [50],
-			success: this.dragState.isMultiDrag ? [50, 25, 50, 25, 50] : [50, 25, 50],
-			cancel: [100]
-		};
-
-		if (this.dragState.sourceType === 'touch' && navigator.vibrate && hapticPatterns[type]) {
-			navigator.vibrate(hapticPatterns[type]);
-		}
-	}
-
-	/**
-	 * Provide consistent feedback for different input methods
-	 */
-	provideFeedback(sourceType, feedbackType, data = {}) {
-		const hapticPatterns = {
-			success: data.isMultiple ? [50, 25, 50, 25, 50] : [50, 25, 50],
-			error: [100, 50, 100]
-		};
-
-		if (sourceType === 'touch' && navigator.vibrate && hapticPatterns[feedbackType]) {
-			navigator.vibrate(hapticPatterns[feedbackType]);
-		}
-	}
-
-	clearDragoverStates() {
-		document.querySelectorAll('.dragover').forEach(el => {
-			el.classList.remove('dragover', 'multi-drop');
-			el.removeAttribute('data-item-count');
-		});
-	}
-	/*********
-	 *  DRAG HANDLERS
-	 ********/
-	handleDragEnter(e) {
-		if (!window.targetCheck(e, '.image.field')) return;
-
-		// Only handle external files
-		if (e.dataTransfer.types.includes('Files')) {
-			e.preventDefault();
-			const uploadContainer = e.target.closest('.file-upload-container');
-			if (uploadContainer) {
-				uploadContainer.classList.add('dragover');
-			}
-		}
-	}
-	handleDragLeave(e) {
-		if (!window.targetCheck(e, '.image.field')) return;
-
-		const uploadContainer = e.target.closest('.file-upload-container');
-		if (uploadContainer && !uploadContainer.contains(e.relatedTarget)) {
-			uploadContainer.classList.remove('dragover');
-		}
-	}
-	handleDragStart(e) {
-		if (!window.targetCheck(e, '.image.field')) return;
-
-		const uploadItem = e.target.closest('[data-upload-id]');
-		if (!uploadItem) return;
-
-		const result = this.startDragOperation({
-			primaryElement: uploadItem,
-			sourceType: 'drag',
-			startPosition: { x: e.clientX, y: e.clientY },
-			event: e
-		});
-
-		if (result) {
-			e.dataTransfer.setData('text/plain', this.dragState.primaryItem);
-			e.dataTransfer.effectAllowed = 'move';
-		} else {
-			e.preventDefault();
-		}
-	}
-
-	handleDragOver(e) {
-		if (!this.dragState.isDragging) return;
-		if (!window.targetCheck(e, '.image.field')) return;
-
-		e.preventDefault();
-		this.updateDragOperation({ x: e.clientX, y: e.clientY }, e.target);
-	}
-
-	handleDrop(e) {
-		if (!window.targetCheck(e, '.image.field')) return;
-
-		e.preventDefault();
-		this.clearDragoverStates();
-
-		// Handle external files (new uploads)
-		const uploadContainer = e.target.closest('.file-upload-container');
-		if (uploadContainer) {
-			const files = Array.from(e.dataTransfer.files);
-			if (files.length > 0) {
-				const fieldId = this.getFieldIdFromElement(uploadContainer);
-				if (fieldId) {
-					this.processFiles(fieldId, files);
-					this.a11y.announce(`${files.length} file(s) dropped for upload`);
-				}
-			}
-		}
-	}
-
-	handleDragEnd(e) {
-		if (!this.dragState.isDragging) return;
-
-		// Find the element under the final drop position
-		const elementUnderDrop = document.elementFromPoint(
-			this.dragState.currentPosition?.x || e.clientX,
-			this.dragState.currentPosition?.y || e.clientY
-		);
-
-		this.endDragOperation(elementUnderDrop);
-	}
-	/*********
-	 * TOUCH HANDLERS
-	 ********/
-	handleTouchStart(e) {
-		if (!window.targetCheck(e, '.image.field')) return;
-		if (this.isTouchOnFormElement(e.target)) {
-			return;
-		}
-
-		const uploadItem = e.target.closest('[data-upload-id]');
-		if (!uploadItem) return;
-
-		const touch = e.touches[0];
-
-		const result = this.startDragOperation({
-			primaryElement: uploadItem,
-			sourceType: 'touch',
-			startPosition: { x: touch.clientX, y: touch.clientY },
-			event: e
-		});
-
-		if (result) {
-			e.preventDefault(); // Prevent scrolling
-		}
-	}
-
-	handleTouchMove(e) {
-		if (!this.dragState.isDragging) return;
-
-		e.preventDefault();
-		const touch = e.touches[0];
-		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-		this.updateDragOperation({ x: touch.clientX, y: touch.clientY }, elementUnderTouch);
-	}
-
-	handleTouchEnd(e) {
-		if (!this.dragState.isDragging) return;
-
-		e.preventDefault();
-		const touch = e.changedTouches[0];
-		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-		this.endDragOperation(elementUnderTouch);
-	}
-
-	handleTouchCancel(e) {
-		if (this.dragState.isDragging) {
-			this.cleanupDragOperation();
-			this.a11y.announce('Drag cancelled');
-		}
+		// Cache the result
+		field._cachedModalType = modalType;
+		return modalType;
 	}
 	/*******************************************************************************
-	 QUEUE INTEGRATION
+	 * GROUP ACTIONS
 	 *******************************************************************************/
-	async submitUploads(fieldId) {
+
+	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':
+				//upload groups
+				let field = this.fields.get(fieldId);
+				field.element.closest('details').open = false;
+				document.body.classList.add('uploading');
+
+				this.submitUploads(fieldId);
+				break;
+			case 'restore':
+				this.handleRestoreUploads().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) {
+			// Create empty group
+			this.createGroup(fieldId);
+		} else {
+			// Create group with selected items
+			const group = this.createGroup(fieldId);
+			if (!group) return;
+
+			selected.forEach(uploadId => {
+				this.addToGroup(uploadId, group.grid);
+			});
+
+			// Clear selection
+			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;
+		}
+
+		// Move items back to preview
+		const items = group.querySelectorAll(this.selectors.items.item);
+		items.forEach(item => {
+			const uploadId = item.dataset.uploadId;
+			this.removeFromGroup(uploadId);
+		});
+
+		// Remove group
+		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
+	 *******************************************************************************/
+
+	/**
+	 * Add selection handler for a field
+	 */
+	addFieldSelectionHandler(fieldId) {
+		if (this.selectionHandlers.has(fieldId)) {
+			return this.selectionHandlers.get(fieldId);
+		}
+
 		const field = this.fields.get(fieldId);
 		if (!field) return;
 
-		// Check if there are uploads to submit
-		const pendingUploads = Array.from(field.uploads || [])
-			.map(id => this.uploads.get(id))
-			.filter(upload => upload &&
-				(upload.status === 'processed' ||
-					upload.status === 'processed-original'));
+		const container = field.ui.field;
+		if (!container) return;
 
-		if (pendingUploads.length === 0) {
-			this.notifications.add('No uploads ready to submit', 'warning');
-			return;
+		const handler = new window.jvbHandleSelection({
+			container: container,
+			ui: {
+				selectAll: container.querySelector('[name="select-all-uploads"]'),
+				bulkControls: container.querySelector('.selection-actions'),
+				count: container.querySelector('.selection-count')
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]'
+		});
+
+		// Subscribe to selection changes
+		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(fieldId, handler);
+		return handler;
+	}
+
+	/**
+	 * Add selection handler for a group
+	 */
+	addGroupSelectionHandler(fieldId, groupId) {
+		const handlerKey = `${fieldId}_${groupId}`;
+
+		if (this.selectionHandlers.has(handlerKey)) {
+			return this.selectionHandlers.get(handlerKey);
 		}
 
-		// Queue the uploads
-		try {
+		const group = this.groups.get(groupId);
+		if (!group) return;
+
+		const handler = new window.jvbHandleSelection({
+			container: group.element,
+			ui: {
+				selectAll: group.element.querySelector(this.selectors.groups.selectAll),
+				bulkControls: group.element.querySelector(this.selectors.groups.actions),
+				count: group.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) {
+	}
+
+	/*******************************************************************************
+	 * HELPER METHODS
+	 *******************************************************************************/
+
+	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', store: this.fields },
+			'upload': { selector: this.selectors.items.item, key: 'uploadId', store: this.uploads },
+			'group': { selector: this.selectors.groups.container, key: 'groupId', store: this.groups }
+		};
+
+		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 config.store.get(id);
+	}
+	getFieldFromElement(el) { return this.getFromElement(el, 'field'); }
+	getUploadFromElement(el) { return this.getFromElement(el, 'upload'); }
+	getGroupFromElement(el) { return this.getFromElement(el, 'group'); }
+
+	getFieldIdFromElement(el) { return this.getFromElement(el, 'field')?.id ?? null};
+	getUploadIdFromElement(el) {return this.getFromElement(el, 'upload')?.id ?? null};
+	getGroupIdFromElement(el) {return this.getFromElement(el, 'group')?.id ?? null};
+
+
+	/*******************************************************************************
+	 * FILE PROCESSING
+	 *******************************************************************************/
+	async processFiles(fieldId, files) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Hide upload container, show group display
+		if (field.ui.dropZone) {
+			field.ui.dropZone.hidden = true;
+		}
+		if (field.ui.groups.display) {
+			field.ui.groups.display.hidden = false;
+		}
+
+		const totalFiles = files.length;
+		let processedCount = 0;
+
+		// Show initial progress
+		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+		// Initialize field uploads set if needed
+		if (!field.uploads) {
+			field.uploads = new Set();
+		}
+
+		// Process files
+		const processPromises = Array.from(files).map(async (file, index) => {
+			try {
+
+				// Create upload ID
+				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+				// Create upload data
+				const uploadData = {
+					id: uploadId,
+					attachment_id: null,
+					fieldId: fieldId,
+					originalFile: file,
+					processedFile: null,
+					preview: null,
+					status: 'local_processing',
+					element: null,
+					location: null,
+					meta: {
+						originalName: file.name,
+						size: file.size,
+						type: file.type
+					}
+				};
+
+				// Create preview URL
+				uploadData.preview = this.createPreviewUrl(file);
+
+				// Process the file (resize if image)
+				if (file.type.startsWith('image/')) {
+					uploadData.processedFile = await this.processImage(file, field.subtype);
+				} else {
+					uploadData.processedFile = file;
+				}
+
+				// Store blob data separately in IndexedDB
+				await this.uploadStore.saveBlob(uploadId, uploadData.processedFile || file);
+
+				// Create DOM element
+				const subtype = this.getSubtypeFromMime(file.type);
+				uploadData.element = this.createUploadElement({
+					...uploadData,
+					subtype: subtype
+				}, field.config.destination === 'post_group');
+
+				// Show progress on the item
+				this.showUploadProgress(uploadId, true);
+				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
+
+				// Add to preview grid
+				if (field.ui.preview) {
+					field.ui.preview.appendChild(uploadData.element);
+					uploadData.location = field.ui.preview;
+				}
+
+				// Store upload
+				this.uploads.set(uploadId, uploadData);
+				field.uploads.add(uploadId);
+
+				// Update progress
+				processedCount++;
+				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				this.updateUploadItemProgress(uploadId, 100, 'processed');
+				uploadData.status = 'processed';
+
+				// Fade out item progress after a moment
+				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;
+			}
+		});
+
+		// Wait for all files to process
+		await Promise.all(processPromises);
+
+		this.updateFieldState(fieldId);
+		// Cache the state (now without DOM references)
+		await this.schedulePersistance(fieldId);
+
+		// Queue for upload if in direct mode
+		if (field.config.destination !== 'post_group') {
 			await this.queueUpload(fieldId);
-			this.notifications.add(`Submitting ${pendingUploads.length} upload(s)`, 'info');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'submitUploads',
-				fieldId
-			});
-			this.notifications.add('Failed to submit uploads', 'error');
+			// Lock uploads if max reached
+			this.maybeLockUploads(fieldId);
+		}
+
+	}
+
+	updateFieldState(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.ui.field) return;
+
+		const container = field.ui.field;
+		const uploadCount = field.uploads?.size || 0;
+		const hasGroups = field.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
+
+		// Set data attributes for CSS targeting
+		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
+		container.dataset.uploadCount = uploadCount.toString();
+		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
+
+		// Update ARIA labels for accessibility
+		if (field.ui.preview) {
+			field.ui.preview.setAttribute('aria-label',
+				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
+			);
 		}
 	}
-	async retryUpload(uploadId) {
+
+	updateUploadProgress(fieldId, current, total, message) {
+		const field = this.fields.get(fieldId);
+		if (!field?.ui?.progress?.progress) return;
+
+		const progress = field.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 field = this.fields.get(fieldId);
+		if (!field) return;
+
+		field.state = status;
+		// Update UI based on status
+	}
+
+	updateUploadStatus(uploadId, status) {
 		const upload = this.uploads.get(uploadId);
 		if (!upload) return;
 
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		try {
-			// Reset status
-			this.updateUploadStatus(uploadId, 'received');
-
-			// If we have the processed file, skip to queuing
-			if (upload.processedFile) {
-				this.updateUploadStatus(uploadId, 'processed');
-				await this.queueUpload(upload.fieldId);
-			} else if (upload.originalFile) {
-				// Reprocess the file
-				const reprocessed = await this.processFile(upload.fieldId, upload.originalFile);
-				if (reprocessed) {
-					await this.queueUpload(upload.fieldId);
-				}
-			} else {
-				throw new Error('No file data available for retry');
-			}
-
-			this.notifications.add('Retrying upload...', 'info');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'retryUpload',
-				uploadId
-			});
-			this.notifications.add('Failed to retry upload', 'error');
-		}
-	}
-	async restartUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field?.uploads) return;
-
-		const failedUploads = Array.from(field.uploads)
-			.map(id => this.uploads.get(id))
-			.filter(upload => upload && upload.status === 'failed');
-
-		if (failedUploads.length === 0) {
-			this.notifications.add('No failed uploads to restart', 'info');
-			return;
-		}
-
-		for (const upload of failedUploads) {
-			await this.retryUpload(upload.id);
-		}
-
-		this.notifications.add(`Restarting ${failedUploads.length} upload(s)`, 'info');
-	}
-	async queueUpload(fieldId) {
-		//Further cache it, or is it already cached at this point?
-		const field = this.fields.get(fieldId);
-		if (!field?.uploads) return;
-
-		const uploads = Array.from(field.uploads);
-		if (uploads.length === 0) {
-			return;
-		}
-
-		const data = this.prepareUploadData(field, uploads);
-		this.a11y.announce('Queuing for upload');
-		let img = (uploads.length === 1) ? 'image' : 'images';
-		const operation = {
-			endpoint: 'uploads',
-			method: 'POST',
-			data: data,
-			title: `Uploading ${uploads.length} ${img} to server...`,
-			popup: `Uploading ${uploads.length} ${img}...`,
-			canMerge: false,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			},
-			append: '_upload'
-		}
-		try {
-			const operationId = await this.queue.addToQueue(operation);
-
-			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
-				if (!upload) {
-					return;
-				}
-				upload.operationId = operationId;
-				this.updateUploadStatus(uploadId, 'queued');
-			});
-			field.operationId = operationId;
-
-			return operationId;
-		} catch (error) {
-			throw error;
-		} finally {
-			this.persistFieldState(field.key);
-		}
+		upload.status = status;
+		this.updateUploadUI(uploadId);
 	}
 
-	prepareUploadData(field, uploads) {
-		console.log('Preparing Upload:', field);
-		const formData = new FormData();
-		formData.append('content', field.content);
-		formData.append('mode', field.mode);
-		formData.append('field_name', field.name);
-		formData.append('field_key', field.key);
-		formData.append('field_type', field.type);
-		formData.append('item_id', field.itemID);		//post, term, or user id
-		formData.append('context', field.context);	//post, term, or user
-		let uploadMap = [];
-		uploads.forEach(uploadId => {
-			let upload = this.uploads.get(uploadId);
-			if (upload) {
-				const fileToUpload = upload.processedFile || upload.originalFile;
-				if (fileToUpload) {
-					formData.append('files[]', fileToUpload);
-					uploadMap.push(upload.id);
-				} else {
-					console.warn(`No file for upload ${uploadId}`);
-				}
-			} else {
-				console.warn(`Upload ${uploadId} not found in uploads map`);
-			}
-		});
-		formData.append('upload_map', uploadMap);
+	updateUploadUI(uploadId) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload?.element) return;
 
-		console.log('Final FormData:');
-		for (let pair of formData.entries()) {
-			console.log(pair[0], pair[1]);
-		}
+		// Update status classes
+		upload.element.className = upload.element.className.replace(/status-[\w-]+/g, '');
+		upload.element.classList.add(`status-${upload.status}`);
 
-		return formData;
-	}
-
-	async queueImageMeta(e) {
-		const upload = this.getUploadFromElement(element);
-		if (!upload) return;
-
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		// Collect meta data from the form
-		const metaContainer = element.closest('.upload-meta');
-		if (!metaContainer) return;
-
-		const metaData = {
-			title: metaContainer.querySelector('[name="title"]')?.value || '',
-			alt_text: metaContainer.querySelector('[name="alt_text"]')?.value || '',
-			caption: metaContainer.querySelector('[name="caption"]')?.value || '',
-			description: metaContainer.querySelector('[name="description"]')?.value || ''
-		};
-
-		// Update upload meta
-		upload.meta = { ...upload.meta, ...metaData };
-		this.uploads.set(upload.id, upload);
-
-		// Mark that we have meta changes
-		this.hasMetaChanges = true;
-
-		// Determine if upload has been sent to server
-		const isOnServer = upload.status === 'completed' && upload.attachmentId;
-
-		if (isOnServer) {
-			// Queue immediate update
-			await this.sendMetaUpdate(upload);
-		} else if (upload.operationId) {
-			// Wait for upload to complete, then send meta
-			this.queueDependentMetaUpdate(upload);
-		} else {
-			// Upload hasn't been queued yet, meta will be sent with initial upload
-			this.persistFieldState(field.key);
+		// Update progress if showing
+		const progress = upload.element.querySelector('.progress');
+		if (progress) {
+			this.updateUploadItemProgress(uploadId,
+				this.getStatusProgress(upload.status),
+				upload.status
+			);
 		}
 	}
 
 	/**
-	 * Send meta update to server
+	 * Show/hide progress indicator on individual upload items
 	 */
-	async sendMetaUpdate(upload) {
-		const formData = new FormData();
-		formData.append('attachment_id', upload.attachmentId);
-		formData.append('title', upload.meta.title);
-		formData.append('alt_text', upload.meta.alt_text);
-		formData.append('caption', upload.meta.caption);
-		formData.append('description', upload.meta.description);
+	showUploadProgress(uploadId, show = true) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload || !upload.element) return;
 
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			data: formData,
-			title: `Updating metadata for ${upload.meta.originalName}`,
-			canMerge: true,
-			headers: {
-				'action_nonce': jvbSettings.dash
+		const progressEl = upload.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);
 			}
-		};
-
-		try {
-			await this.queue.addToQueue(operation);
-			this.notifications.add('Metadata updated', 'success');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'sendMetaUpdate',
-				uploadId: upload.id
-			});
 		}
 	}
 
 	/**
-	 * Queue meta update that depends on upload completion
+	 * Update individual upload progress bar
 	 */
-	queueDependentMetaUpdate(upload) {
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			dependencies: [upload.operationId],
-			data: () => {
-				// This function will be called when dependencies are resolved
-				const formData = new FormData();
-				formData.append('operation_id', upload.operationId);
-				formData.append('upload_id', upload.id);
-				formData.append('title', upload.meta.title);
-				formData.append('alt_text', upload.meta.alt_text);
-				formData.append('caption', upload.meta.caption);
-				formData.append('description', upload.meta.description);
-				return formData;
-			},
-			title: `Updating metadata after upload`,
-			canMerge: true,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			}
-		};
+	updateUploadItemProgress(uploadId, percent, status = null) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload || !upload.element) return;
 
-		this.queue.addToQueue(operation);
+		const progressEl = upload.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;
+		}
 	}
-	/*******************************************************************************
-	 IMAGE PROCESSING
-	*******************************************************************************/
-	async processFiles(fieldId, files) {
-		const field = this.fields.get(fieldId);
-		if(!field) return;
-
-		//Validate Files
-		const validFiles = files.filter(file=>this.validateFile(file, field));
-		if (validFiles.length === 0) return;
-
-		if (!this.checkFieldLimits(fieldId, validFiles.length)) {
-			// this.notify(`Cannot add ${validFiles.length} files. Field limit exceeded.`, 'warning');
-			return;
-		}
-		const processedUploads = await this.processBatch(fieldId, validFiles);
-
-		this.maybeLockUploads(fieldId);
-
-		if (field.groupDisplay) {
-			field.groupDisplay.hidden = false;
-		}
-		if (processedUploads.length > 0) {
-			await this.queueUpload(fieldId);
-		}
-
-		this.hideUploadProgress(fieldId);
-
-		this.a11y.announce(`Processed ${processedUploads.length} of ${validFiles.length} files`);
-	}
-
 	checkFieldLimits(fieldId, additionalFiles) {
 		const field = this.fields.get(fieldId);
 		if (!field) return false;
@@ -1423,18 +1065,9 @@
 		const currentCount = field.uploads?.size || 0;
 		const totalCount = currentCount + additionalFiles;
 
-		if (totalCount > field.maxFiles) {
-			this.notifications.add(
-				`Cannot add ${additionalFiles} files. Max ${field.maxFiles} allowed, currently have ${currentCount}.`,
-				'warning'
-			);
-			return false;
-		}
+		return totalCount <= field.maxFiles;
 
-		return true;
-	}
-	generateUploadId() {
-		return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
 	}
 	validateFile(file, field) {
 		// Type validation
@@ -1464,101 +1097,14 @@
 		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
 	}
 
-	async processBatch(fieldId, files) {
-		const results = [];
-		const processingQueue = [];
-		const maxConcurrent = this.worker.settings.maxConcurrent;
-
-		let total = files.length;
-		for (let i = 0; i < files.length; i++) {
-			this.updateUploadProgress(fieldId, i, total);
-			// Wait if we've reached max concurrent processing
-			if (processingQueue.length >= maxConcurrent) {
-				await Promise.race(processingQueue);
-			}
-
-			const processPromise = this.processFile(fieldId, files[i])
-				.then(upload => {
-					// Remove from processing queue
-					const index = processingQueue.indexOf(processPromise);
-					if (index > -1) processingQueue.splice(index, 1);
-
-					if (upload) results.push(upload);
-					return upload;
-				})
-				.catch(error => {
-					console.error(`Failed to process ${files[i].name}:`, error);
-					// Remove from processing queue
-					const index = processingQueue.indexOf(processPromise);
-					if (index > -1) processingQueue.splice(index, 1);
-					return null;
-				});
-
-			processingQueue.push(processPromise);
+	shouldProcessClientSide(file, subtype) {
+		// Only process images client-side
+		if (subtype === 'image' && file.type.startsWith('image/')) {
+			return true;
 		}
 
-		// Wait for remaining files
-		await Promise.all(processingQueue);
-		return results;
-	}
-
-	async processFile(fieldId, file) {
-		const field = this.fields.get(fieldId);
-
-		const upload = await this.setUpload(fieldId, file);
-		const uploadId = upload.id;
-		try {
-			// Update UI immediately
-			this.addImageToGroup(uploadId);
-			this.updateUploadStatus(uploadId, 'local_processing');
-
-			// Attempt to process the image
-			let processedFile = null;
-			let processingFailed = false;
-
-			try {
-				processedFile = await this.processImage(file, uploadId);
-			} catch (error) {
-				console.warn(`Processing failed for ${file.name}, using original:`, error);
-				processingFailed = true;
-				processedFile = file; // Use original
-			}
-
-			// Update upload with processed file
-			upload.processedFile = processedFile;
-			upload.processingFailed = processingFailed;
-
-			// Update status
-			this.updateUploadStatus(uploadId, 'processed');
-
-			// Save to uploads map
-			this.uploads.set(uploadId, upload);
-
-			// Persist state
-			if (field && field.key) {
-				await this.persistFieldState(field.key);
-			}
-
-			const message = processingFailed
-				? `${file.name} added (original format)`
-				: `${file.name} processed and ready`;
-			this.a11y.announce(message);
-
-			return upload;
-
-		} catch (error) {
-			// Clean up failed upload
-			this.cleanupFailedUpload(uploadId, field.key);
-
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'processFile',
-				uploadId,
-				fileName: file.name
-			});
-
-			return null;
-		}
+		// Videos and documents go straight to server
+		return false;
 	}
 
 	async processImage(file, uploadId) {
@@ -1705,7 +1251,7 @@
 			};
 
 			try {
-				objectUrl = URL.createObjectURL(file);
+				objectUrl = this.createPreviewUrl(file);
 				img.src = objectUrl;
 			} catch (error) {
 				cleanup();
@@ -1833,8 +1379,6 @@
 	 * Restart compression worker
 	 */
 	restartCompressionWorker() {
-		console.log('Restarting compression worker...');
-
 		// Terminate existing worker
 		if (this.worker.worker) {
 			this.worker.worker.terminate();
@@ -1912,7 +1456,7 @@
         `;
 
 			const blob = new Blob([workerScript], { type: 'application/javascript' });
-			this.worker.worker = new Worker(URL.createObjectURL(blob));
+			this.worker.worker = new Worker(this.createPreviewUrl(blob));
 
 		} catch (error) {
 			console.warn('Failed to initialize compression worker:', error);
@@ -1949,120 +1493,62 @@
 		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
 	}
 
-	/**
-	 * Clean up failed upload
-	 */
-	cleanupFailedUpload(uploadId, fieldId) {
-		const field = this.fields.get(fieldId);
-		if (field?.uploads) {
-			field.uploads.delete(uploadId);
-		}
-
-		const upload = this.uploads.get(uploadId);
-		if (upload) {
-			// Clean up preview URL
-			if (upload.preview?.startsWith('blob:')) {
-				URL.revokeObjectURL(upload.preview);
-			}
-
-			// Remove element
-			upload.element?.remove();
-
-			// Remove from uploads
-			this.uploads.delete(uploadId);
-		}
-
-		// Remove from active tasks
-		this.worker.tasks.delete(uploadId);
+	createPreviewUrl(file) {
+		const url = URL.createObjectURL(file);
+		// Track for cleanup
+		if (!this.previewUrls) this.previewUrls = new Set();
+		this.previewUrls.add(url);
+		return url;
 	}
-	/*******************************************************************************
-	 UI FUNCTIONALITY
-	*******************************************************************************/
-	/**
-	 * Update upload status correctly
-	 */
-	updateUploadStatus(uploadId, status) {
-		console.log('Updating upload status for: ', uploadId);
-		let upload = this.uploads.get(uploadId);
-		if(!upload) {
-			return;
-		}
-		upload.status = status;
 
-		this.updateImageUI(upload.id);
-		this.persistFieldState(upload.fieldId);
-	}
-	updateImageUI(uploadId) {
-		console.log('Updating image UI: ', uploadId);
-		const upload = this.uploads.get(uploadId);
-		console.log(upload);
-		if (!upload?.element) return;
-
-
-		const progressEl = upload.element.querySelector('.progress');
-		const itemEl = upload.element;
-
-		console.log('Updating Upload UI:', upload);
-		// Update status class on item for CSS styling
-		if (itemEl) {
-			itemEl.className = itemEl.className.replace(/status-[\w-]+/g, '');
-			itemEl.classList.add(`status-${upload.status}`);
-		}
-
-		if (progressEl) {
-			let icon = this.getStatusIcon(upload.status);
-			let message = this.getStatusText(upload.status);
-			let progress = this.getStatusProgress(upload.status);
-
-			const fill = progressEl.querySelector('.fill');
-			const itemIcon = progressEl.querySelector('span.icon');
-			const itemMessage = progressEl.querySelector('span.details');
-
-			if (fill) {
-				fill.style.width = `${progress}%`;
-			}
-			if (itemMessage) itemMessage.textContent = message;
-			if (itemIcon) {
-				window.removeChildren(itemIcon);
-				itemIcon.append(icon);
-			}
-
-			if (upload.status === 'completed') {
-				setTimeout(() => {
-					if (progressEl) {
-						window.fade(progressEl, false);
-					}
-				}, 1000);
-			}
+	revokePreviewUrl(url) {
+		if (url?.startsWith('blob:')) {
+			URL.revokeObjectURL(url);
+			this.previewUrls?.delete(url);
 		}
 	}
-	/**
-	 * Hide the uploader drop zone if we have reached our limit
-	 */
+
 	maybeLockUploads(fieldId) {
 		const field = this.fields.get(fieldId);
-		if (!field) return;
+		if (!field?.ui?.dropZone) return;
 
-		// Hide/show drop zone based on file count
-		if (field.ui.field.dropZone) {
-			field.ui.field.dropZone.hidden = field.uploads && field.uploads.size >= field.maxFiles;
+		if (field.config.destination === 'post_group') {
+			return;
 		}
+
+		const uploadCount = field.uploads?.size || 0;
+		const maxFiles = field.config?.maxFiles || 999;
+
+		// Hide dropzone if at max files
+		field.ui.dropZone.hidden = uploadCount >= maxFiles;
+
+		// Update field state
+		field.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
 	}
-	createImageElement(upload, draggable = false) {
+	createUploadElement(upload, draggable = false) {
 		let image = window.getTemplate('uploadItem');
 		if (!image) {
 			console.error('Image template not found');
 			return;
 		}
 		image.dataset.uploadId = upload.id;
+		if (upload.originalFile) {
+			image.dataset.subtype = this.getSubtypeFromMime(upload.originalFile.type);
+		}
+
+
 		image.querySelector('[name="featured"]').value = upload.id;
 		let [
 			featured,
 			img,
+			video,
+			preview,
 			details
 		] = [
 			image.querySelector('[name="featured"]'),
 			image.querySelector('img'),
+			image.querySelector('video'),
+			image.querySelector('label > span'),
 			image.querySelector('details')
 		];
 		[
@@ -2074,6 +1560,45 @@
 			upload.preview,
 			upload.originalFile?.name ?? upload.meta?.originalName ?? '',
 		];
+
+		switch (image.dataset.subtype) {
+			case 'image':
+				[
+					img.src,
+					img.alt
+				] = [
+					upload.preview,
+					upload.originalFile?.name ?? upload.meta?.originalName?? ''
+				];
+				video.remove();
+				preview.remove();
+				break;
+			case 'video':
+				video.src = upload.preview;
+				img.remove();
+				preview.remove();
+				break;
+			case 'document':
+				const fileName = upload.originalFile?.name ?? 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');
+
+				preview.innerText = upload.originalFile.name;
+				preview.prepend(icon);
+				img.remove();
+				video.remove();
+				break;
+		}
 		if (details) {
 			let template = window.getTemplate('uploadMeta');
 			if (template){
@@ -2097,345 +1622,740 @@
 
 		return image;
 	}
-
-	updateUploadProgress(fieldId, current, total, message) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		let progressBar = field.ui.field.progress.progress;
-
-		// Create progress bar if it doesn't exist
-		if (!progressBar) {
-			progressBar = window.getTemplate('imageProgress');
-
-			// Insert after drop zone or at top of container
-			const insertAfter = field.dropZone || field.container.firstElementChild;
-			if (insertAfter) {
-				insertAfter.insertAdjacentElement('afterend', progressBar);
-			} else {
-				field.container.prepend(progressBar);
-			}
-		}
-
-		// Update progress bar
-		const progressPercent = total > 0 ? Math.round((current / total) * 100) : 0;
-		const progressFill = field.ui.field.progress.fill;
-		const progressMessage = field.ui.field.progress.details;
-		const progressCount = field.ui.field.progress.count;
-
-		if (progressFill) {
-			progressFill.style.width = `${progressPercent}%`;
-		}
-
-		if (progressMessage) {
-			progressMessage.textContent = message;
-		}
-
-		if (progressCount) {
-			progressCount.textContent = `${current}/${total}`;
-		}
-
-		// Add completion styling
-		if (current === total) {
-			progressBar.classList.add('completed');
-		}
-	}
-
-	hideUploadProgress(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const progressBar = field.ui.field.progress.progress;
-		if (progressBar) {
-			window.fade(progressBar, false);
-		}
-	}
 	/*******************************************************************************
-	 INDEXEDDB CACHE FUNCTIONALITY
-	*******************************************************************************/
-	async initDB() {
-		if (!('indexedDB' in window)) return;
+	 * QUEUE INTEGRATION
+	 *******************************************************************************/
+	async submitUploads(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field?.uploads || field.uploads.size === 0) {
+			return;
+		}
 
-		const request = indexedDB.open(`jvb_uploads_db`, 1);
+		let uploads = Array.from(field.uploads);
+		if (uploads.length === 0) {
+			this.error.log('No uploads to upload', {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			return;
+		}
 
-		request.onupgradeneeded = (e) => {
-			const db = e.target.result;
-			if (!db.objectStoreNames.contains('fieldStates')) {
-				const store = db.createObjectStore('fieldStates', { keyPath: 'fieldId' });
-				store.createIndex('timestamp', 'timestamp', { unique: false });
-				store.createIndex('content', 'content', { unique: false });
-				store.createIndex('itemId', 'itemId', { unique: false });
+		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 = [];
+
+		uploads = uploads.map((upload) => {
+			return this.uploads.get(upload);
+		});
+
+		fieldGroups.forEach((group, groupIndex) => {
+			const post = {
+				images: [],
+				fields: {}
+			};
+			for (let [name, value] of Object.entries(group.changes)) {
+				post.fields[name] = value;
 			}
 
-			// Blob storage remains separate for performance
-			if (!db.objectStoreNames.contains('uploadBlobs')) {
-				db.createObjectStore('uploadBlobs', { keyPath: 'uploadId' });
+			let groupUploads = uploads.filter((upload) => {
+				return upload['groupId'] === group.id;
+			});
+
+			groupUploads.forEach((upload) => {
+				if (upload) {
+					const fileToUpload = upload.processedFile || upload.originalFile;
+					if (fileToUpload) {
+						formData.append('files[]', fileToUpload);
+
+						const imageData = {
+							upload_id: upload.id,
+							index: uploadMap.length
+						};
+						post.images.push(imageData);
+						uploadMap.push(upload.id);
+					}
+				}
+			});
+			// Add images for this group
+			// group.uploads.forEach(uploadId => {
+			// 	const upload = this.uploads.get(uploadId);
+			// 	if (upload) {
+			// 		const fileToUpload = upload.processedFile || upload.originalFile;
+			// 		if (fileToUpload) {
+			// 			formData.append('files[]', fileToUpload);
+			//
+			// 			const imageData = {
+			// 				upload_id: upload.id,
+			// 				index: uploadMap.length
+			// 			};
+			//
+			// 			// Check if this is the featured image
+			// 			const radioInput = upload.element?.querySelector('[name="featured"]');
+			// 			if (radioInput?.checked) {
+			// 				post.fields.featured = upload.id;
+			// 			}
+			//
+			// 			post.images.push(imageData);
+			// 			uploadMap.push(upload.id);
+			// 		}
+			// 	}
+			// });
+
+			posts.push(post);
+		});
+
+		//Each remaining upload (without a groupId) becomes its own post
+		let remainingUploads = uploads.filter((upload) => {
+			return !Object.hasOwn(upload, 'groupId');
+		});
+
+		remainingUploads.forEach((upload) => {
+			if (upload) {
+
+				const post = {
+					images: [],
+					fields: {}
+				};
+				const fileToUpload = upload.processedFile || upload.originalFile;
+				if (fileToUpload) {
+					formData.append('files[]', fileToUpload);
+
+					const imageData = {
+						upload_id: upload.id,
+						index: uploadMap.length
+					};
+					post.images.push(imageData);
+					uploadMap.push(upload.id);
+				}
+				posts.push(post);
+			}
+		});
+
+
+		// Add metadata to FormData
+		formData.append('content', field.config.content);
+		formData.append('user', field.config.itemID); // Assuming itemID is user ID
+		formData.append('posts', JSON.stringify(posts));
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+
+		for (const [key, value] of formData.entries()) {
+			console.log(key, value);
+		}
+		const operation = {
+			endpoint: 'uploads/groups',
+			method: 'POST',
+			data: formData,
+			title: `Creating ${posts.length} ${field.config.content}${posts.length > 1 ? 's' : ''} from uploads...`,
+			popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
+			canMerge: false,
+			headers: {
+				'action_nonce': jvbSettings.dash
+			},
+			append: '_upload',
+		};
+
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			uploads.forEach(uploadId => {
+				let upload = this.uploads.get(uploadId);
+				if (upload) {
+					upload.operationId = operationId;
+					this.updateUploadStatus(uploadId, 'queued');
+				}
+			});
+
+			field.operationId = operationId;
+			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;
+		} finally {
+			this.schedulePersistance(field.id);
+		}
+	}
+
+	async queueUpload(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field?.uploads) return;
+
+		const uploads = Array.from(field.uploads);
+		if (uploads.length === 0) {
+			return;
+		}
+
+		const data = this.prepareUploadData(field, uploads);
+		this.a11y.announce('Queuing for upload');
+		let img = (uploads.length === 1) ? 'file' : 'files';
+		const operation = {
+			endpoint: 'uploads',
+			method: 'POST',
+			data: data,
+			title: `Uploading ${uploads.length} ${img} to server...`,
+			popup: `Uploading ${uploads.length} ${img}...`,
+			canMerge: false,
+			headers: {
+				'action_nonce': jvbSettings.dash
+			},
+			append: '_upload'
+		}
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			uploads.forEach(uploadId => {
+				let upload = this.uploads.get(uploadId);
+				if (!upload) {
+					return;
+				}
+				upload.operationId = operationId;
+				this.updateUploadStatus(uploadId, 'queued');
+			});
+			field.operationId = operationId;
+
+			return operationId;
+		} catch (error) {
+			throw error;
+		} finally {
+			this.schedulePersistance(field.id);
+		}
+	}
+
+	prepareUploadData(field, uploads) {
+
+		const formData = new FormData();
+		formData.append('content', field.config.content);
+		formData.append('mode', field.config.mode);
+		formData.append('field_name', field.config.name);
+		formData.append('fieldId', field.id);
+		formData.append('field_type', field.config.type);
+		formData.append('subtype', field.config.subtype);
+		formData.append('item_id', field.config.itemID);		//post, term, or user id
+		formData.append('destination', field.config.destination || 'meta'); //meta, post, post_group
+		let uploadMap = [];
+
+		const fieldGroups = this.getFieldGroups(field.id);
+		if (field.config.destination === 'post_group' && fieldGroups.length > 0) {
+			// User has created groups
+			let groups = [];
+			let titles = [];
+			let featuredImages = [];
+
+			fieldGroups.forEach(group => {
+				let groupUploadIndices = [];
+				let featuredIndex = null;
+
+				group.uploads.forEach(uploadId => {
+					let upload = this.uploads.get(uploadId);
+					if (upload) {
+						const fileToUpload = upload.processedFile || upload.originalFile;
+						if (fileToUpload) {
+							formData.append('files[]', fileToUpload);
+							const fileIndex = uploadMap.length;
+							uploadMap.push(upload.id);
+							groupUploadIndices.push(upload.id);
+
+							// Check if this is the featured image
+							const radioInput = upload.element?.querySelector('[name="featured"]');
+							if (radioInput?.checked) {
+								featuredIndex = upload.id;
+							}
+						}
+					}
+				});
+
+				groups.push(groupUploadIndices);
+				titles.push(group.title || '');
+				featuredImages.push(featuredIndex);
+			});
+
+			formData.append('groups', JSON.stringify(groups));
+			formData.append('group_titles', JSON.stringify(titles));
+			formData.append('featured_images', JSON.stringify(featuredImages));
+		} else {
+			// No groups - just append all files
+			uploads.forEach(uploadId => {
+				let upload = this.uploads.get(uploadId);
+				if (upload) {
+					const fileToUpload = upload.processedFile || upload.originalFile;
+					if (fileToUpload) {
+						formData.append('files[]', fileToUpload);
+						uploadMap.push(upload.id);
+					}
+				}
+			});
+		}
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+
+		// console.log('Final FormData:');
+		// for (let pair of formData.entries()) {
+		// 	console.log(pair[0], pair[1]);
+		// }
+
+		return formData;
+	}
+
+	getFieldGroups(fieldId) {
+		const groups = [];
+
+		this.groups.forEach((groupData, groupId) => {
+			if (groupData.fieldId === fieldId) {
+				const field = this.fields.get(fieldId);
+				const groupElement = field?.ui?.groups?.groups?.get(groupId);
+
+				groups.push({
+					id: groupId,
+					uploads: Array.from(groupData.uploads || new Set()),
+					changes: groupData.changes || {},
+					element: groupElement || null
+				});
+			}
+		});
+
+		return groups;
+	}
+
+	async queueUploadMeta(e) {
+		const upload = this.getUploadFromElement(e.target);
+		if (!upload) return;
+
+		const field = this.fields.get(upload.fieldId);
+		if (!field) return;
+
+		const container = e.target.closest('.upload-meta');
+		if (!container) return;
+
+		let data = {};
+		data[e.target.name] = e.target.value;
+
+		upload.meta = {
+			...upload.meta,
+			... data
+		};
+
+		let queueData = {};
+		//If there is an attachment ID, use that: else, use our generated upload id
+		queueData[upload.attachmentId??upload.id] = upload.meta;
+
+		const operation = {
+			endpoint:	'uploads/meta',
+			method:		'POST',
+			data:		queueData,
+			title:		`Updating meta`,
+			canMerge:	true,
+			headers:	{
+				'action_nonce': jvbSettings.dash
 			}
 		};
 
-		request.onsuccess = (e) => {
-			this.db = e.target.result;
-			this.loadFields();
-		};
-
-		request.onerror = (e) => {
-			console.error('IndexedDB error:', e);
-		};
-	}
-
-	async loadFields() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readonly');
-			const fieldStates = tx.objectStore('fieldStates');
-			const blobStore = tx.objectStore('uploadBlobs');
-			const request = fieldStates.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(field => {
-					let uploads = field.uploads;
-					let uploadIds = uploads.map(upload => upload.id);
-					field.uploads = new Set(uploadIds);
-					this.fields.set(field.key, field);
-					uploads.forEach(upload => {
-						this.uploads.set(upload.id, upload);
-					});
-				});
-				this.notify('uploads-loaded', { items: Array.from(this.uploads.values()) });
-				resolve();
-			};
-
-			const blobRequest = blobStore.getAll();
-
-			blobRequest.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					this.uploadBlobs.set(item.id, item);
-				});
-				this.notify('blobs-loaded', { items: Array.from(this.uploadBlobs.values()) });
-				resolve();
-			};
-		});
-	}
-
-	getUpload(uploadId) {
-		return this.uploads.get(uploadId);
-	}
-
-	clearField(fieldId) {
-		let uploads = Array.from(this.fields.uploads);
-		uploads.forEach(upload => {
-			this.uploads.delete(upload);
-		});
-		this.fields.delete(fieldId);
-		if (this.db) {
-			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
-			tx.objectStore('fieldStates').delete(fieldId);
-			uploads.forEach(upload => {
-				tx.objectStore('uploadBlobs').delete(upload);
+		try {
+			await this.queue.addToQueue(operation);
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'sendMetaUpdate',
+				uploadId: upload.id
 			});
 		}
 	}
+	/*******************************************************************************
+	 * GROUP MANAGEMENT
+	 *******************************************************************************/
 
-	updateFieldStatus(fieldId, status) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		field.uploads.forEach(upload => {
-			console.log('Attempting to set upload to status: ', status);
-			this.updateUploadStatus(upload, status);
-		});
-
-		// Update UI based on status
-		const container = field.ui.field.field;
-		if (container) {
-			container.dataset.uploadStatus = status;
-
-			// Show/hide relevant UI elements
-			const submitBtn = container.querySelector('.submit-uploads');
-			if (submitBtn) {
-				submitBtn.disabled = status === 'uploading' || status === 'processing';
-			}
-		}
-	}
-
-	/**
-	 * Handle successful upload completion
-	 */
-	handleUploadComplete(operation) {
-		const response = operation.response;
-		if (!response?.uploads) return;
-
-		// Map server IDs to uploads
-		response.uploads.forEach(serverUpload => {
-			const upload = this.uploads.get(serverUpload.upload_id);
-			if (upload) {
-				upload.attachmentId = serverUpload.attachment_id;
-				this.updateUploadStatus(serverUpload.upload_id, 'completed');
-				this.uploads.set(upload.id, upload);
-
-				// Clear from cache since it's now on server
-				this.clearUpload(upload.id);
-			}
-		});
-
-		// Persist updated field state
-		const fieldKey = operation.data.get('field_key');
-		if (fieldKey) {
-			this.persistFieldState(fieldKey);
-		}
-	}
-
-	/**
-	 * Clear individual upload from cache after successful server upload
-	 */
-	async clearUpload(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
-
-		// Clean up preview URL
-		if (upload.preview?.startsWith('blob:')) {
-			URL.revokeObjectURL(upload.preview);
+	createGroup(fieldKey, groupId = null) {
+		const field = this.fields.get(fieldKey);
+		if (!field) {
+			console.error('Field not found:', fieldKey);
+			return null;
 		}
 
-		this.persistFieldState(upload.fieldId);
-		// Remove from memory
-		this.uploads.delete(uploadId);
-		this.uploadBlobs.delete(uploadId);
-
-		// Remove from IndexedDB
-		if (this.db) {
-			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
-			await tx.objectStore('uploadBlobs').delete(uploadId);
+		if (!groupId) {
+			groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
 		}
-	}
 
-	/**
-	 * Store upload with DataStore integration
-	 */
-	async setUpload(fieldId, file, uploadId = null) {
-		if (!uploadId) {
-			uploadId = this.generateUploadId();
+		const groupElement = this.createGroupElement(groupId, fieldKey);
+		if (!groupElement) {
+			console.error('Failed to create group element');
+			return null;
 		}
-		const upload = {
-			id: uploadId,
-			fieldId: fieldId,
-			groupId: null,
-			originalFile: file,
-			processedFile: null,
-			status: 'received',
-			progress: { percent: 0, message: 'Received...' },
-			preview: URL.createObjectURL(file),
-			createdAt: Date.now(),
-			meta: {
-				title: '',
-				alt_text: '',
-				caption: '',
-				originalName: file.name,
-				originalType: file.type,
-				originalSize: file.size
-			},
+
+		// Store in field UI Map
+		if (!field.ui.groups) {
+			field.ui.groups = {
+				groups: new Map(),
+				container: null,
+				empty: null,
+				display: null
+			};
+		}
+
+		field.ui.groups.groups.set(groupId, groupElement);
+
+		// Insert into DOM
+		if (field.ui.groups.container && field.ui.groups.empty) {
+			field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
+		} else if (field.ui.groups.container) {
+			field.ui.groups.container.appendChild(groupElement);
+		}
+
+		// Create group object
+		const group = {
+			id: groupId,
+			fieldId: fieldKey,
+			element: groupElement,
+			grid: groupElement.querySelector('.item-grid.group'),
+			uploads: new Set(),
 			changes: {}
 		};
 
-		// Add to field
-		const field = this.fields.get(fieldId);
-		if (!field) {
-			console.error(`Field ${fieldId} not found`);
-			return null;
-		}
-		if (!field.uploads) field.uploads = new Set();
-		field.uploads.add(uploadId);
+		// Store group
+		this.groups.set(groupId, group);
 
-		upload.element = this.createImageElement(upload, field.type==='groupable');
-		upload.ui = window.uiFromSelectors(this.selectors.item, upload.element);
+		// Initialize selection handler for this group
+		this.addGroupSelectionHandler(fieldKey, groupId);
 
-		// Store in memory
-		this.uploads.set(uploadId, upload);
-		this.updateImageUI(uploadId);
+		// Persist state
+		this.schedulePersistance(fieldKey);
 
-		// Persist to DataStore
-		await this.persistFieldState(fieldId);
-
-		return upload;
+		return group;
 	}
 
-	getFieldUploads(fieldId, stripElements) {
+	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);
+
+			// Set unique IDs and names for form 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]`;
+			}
+			let field = this.fields.get(fieldId);
+			if (field.config.content !== '') {
+				let summary = groupElement.querySelector('summary');
+				summary.textContent = field.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) {
+		let group = this.groups.get(groupId);
+		if (!group) {
+			return;
+		}
+
+		let keepUploads = true;
+		if (confirm && group.uploads && group.uploads.size > 0) {
+			keepUploads = !window.confirm('Delete uploads in group?');
+		}
+
+		if (confirm && keepUploads) {
+			// Move any remaining uploads back to preview
+			if (group.uploads && group.uploads.size > 0) {
+				Array.from(group.uploads).forEach(uploadId => {
+					this.addImageToGroup(uploadId, null, false);
+				});
+			}
+		}
+
+		// Remove from groups Map
+		this.groups.delete(groupId);
+
+		// Remove DOM element
+		let groupElement = group.element;
+		if (groupElement) {
+			groupElement.remove();
+			this.a11y.announce('Group removed');
+		}
+
+		this.schedulePersistance(group.fieldId);
+	}
+
+	addToGroup(uploadId, target = null, persist = true) {
+		let upload = this.uploads.get(uploadId);
+		if(!upload) {
+			return;
+		}
+		let field = this.fields.get(upload.fieldId);
+		if (!field) {
+			return;
+		}
+
+		//Already in the Preview Grid, or already in the group we're moving to
+		if ((!target && upload.location === field.ui.preview) || target === upload.location) {
+			return;
+		}
+
+		// Remove from previous location
+		if (upload.location) {
+			let groupId = upload.location.dataset.groupId;
+			if (groupId) {
+				let group = this.groups.get(groupId);
+				if (group && group.uploads) {
+					group.uploads.delete(uploadId);
+
+					if (group.uploads.size === 0) {
+						this.deleteGroup(groupId);
+					}
+				}
+			}
+		}
+
+		const checkbox = upload.element.querySelector('[name*="select-item"]');
+		if (checkbox) {
+			checkbox.checked = false;
+		}
+
+		let featured = upload.element.querySelector('[name="featured"]');
+		featured.hidden = !target;
+
+
+		//If no target, it's going to the preview grid
+		if (!target) {
+			target = field.ui.preview;
+			upload.groupId = null;
+		} else if (!target.classList.contains('item-grid') || !target.classList.contains('preview')) {
+			// It's a group target
+			let groupId = target.dataset.groupId;
+			featured.name = groupId+'_'+featured.name;
+			let group = this.groups.get(groupId);
+			if (!group) {
+				group = this.createGroup(upload.fieldId);
+				target = group.grid;
+				groupId = group.id;
+			}
+			if (group) {
+				group.uploads.add(uploadId);
+				upload.groupId = groupId;
+			}
+
+		}
+
+		upload.location = target;
+		target.append(upload.element);
+
+		if (persist) {
+			this.schedulePersistance(field.id);
+		}
+	}
+
+	removeFromGroup(uploadId) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload) return;
+
+		const field = this.fields.get(upload.fieldId);
+		if (!field) return;
+
+		// Remove from current group if in one
+		if (upload.groupId) {
+			const group = this.groups.get(upload.groupId);
+			if (group?.uploads) {
+				group.uploads.delete(uploadId);
+
+				// Delete empty group
+				if (group.uploads.size === 0) {
+					this.deleteGroup(upload.groupId, false);
+				}
+			}
+			upload.groupId = null;
+		}
+
+		// Move back to preview
+		if (field.ui?.preview) {
+			field.ui.preview.appendChild(upload.element);
+			upload.location = field.ui.preview;
+		}
+
+		// Hide featured radio
+		const featured = upload.element.querySelector('[name="featured"]');
+		if (featured) {
+			featured.hidden = true;
+			featured.checked = false;
+		}
+	}
+
+	removeUpload(fieldId, uploadId) {
 		const field = this.fields.get(fieldId);
-		console.log('Got field uploads: ', field);
-		if (!field?.uploads) return [];
+		const upload = this.uploads.get(uploadId);
+
+		if (!field || !upload) return;
+
+		// Remove from field
+		field.uploads?.delete(uploadId);
+
+		// Remove from group if grouped
+		if (upload.groupId) {
+			const group = this.groups.get(upload.groupId);
+			if (group && group.uploads) {
+				group.uploads.delete(uploadId);
+
+				if (group.uploads.size === 0) {
+					this.removeGroup(upload.groupId);
+				}
+			}
+		}
+
+		// Clean up element
+		upload.element?.remove();
+
+		// Clean up memory
+		this.clearUpload(uploadId);
+
+		// Update field state after removal
+		this.updateFieldState(fieldId);
+
+		// Update UI
+		this.maybeLockUploads(fieldId);
+		const handler = this.selectionHandlers.get(field.id);
+		if (handler) {
+			handler.deselect(uploadId);
+		}
+
+		this.a11y.announce('Upload removed');
+	}
+
+	/*******************************************************************************
+	 * STATE MANAGEMENT
+	 *******************************************************************************/
+	schedulePersistance(fieldId) {
+		const key = `persist_${fieldId}`;
+		window.debouncer.schedule(
+			key,
+			() => this.persistFieldState(fieldId),
+			1000
+		);
+	}
+
+	async persistFieldState(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Convert Sets to Arrays for storage
+		const fieldData = {
+			...field,
+			id: fieldId, // Use as primary key
+			fieldId: fieldId,
+			uploads: Array.from(field.uploads || []).map(uploadId => {
+				return this.uploads.get(uploadId);;
+			}),
+			groups: Array.from(this.groups.entries())
+				.filter(([id, data]) => data.fieldId === fieldId && data.uploads && data.uploads.size > 0)
+				.map(([id, data]) => ({
+					id: data.id,
+					uploads: Array.from(data.uploads),
+					changes: data.changes || {}
+				})),
+
+			// Context for restoration
+			context: {
+				url: this.normalizeUrl(window.location.href),
+				fullUrl: window.location.href,
+				modalType: this.getModalType(field),
+				formId: field.formId,
+				fieldSelector: `.field.upload[data-field="${field.config.name}"]`
+			},
+			timestamp: Date.now()
+		};
+
+		// Save to store
+		await this.fieldStore.save(fieldData);
+	}
+	normalizeUrl(url) {
+		try {
+			const urlObj = new URL(url);
+			// Return just the origin + pathname (no query string or hash)
+			return urlObj.origin + urlObj.pathname;
+		} catch (e) {
+			return url;
+		}
+	}
+
+	/**
+	 * Get uploads for a field, optionally cleaned for storage
+	 * @param {string} fieldId
+	 * @param {boolean} clean - Remove DOM references for IndexedDB storage
+	 * @returns {Array}
+	 */
+	getFieldUploads(fieldId, clean = false) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.uploads) return [];
 
 		return Array.from(field.uploads)
-			.map(id => {
-				let upload = this.uploads.get(id);
+			.map(uploadId => {
+				const upload = this.uploads.get(uploadId);
 				if (!upload) return null;
-				if (stripElements) {
-					// Create a clean copy without DOM references
-					const { element, ui, ...cleanUpload } = upload;
-					upload = cleanUpload;
+
+				if (clean) {
+					// Return cleaned version without DOM references or blob URLs
+					return {
+						id: upload.id,
+						fieldId: upload.fieldId,
+						status: upload.status,
+						// DON'T include preview (blob URL)
+						// DON'T include originalFile or processedFile (in blob storage)
+						attachmentId: upload.attachmentId,
+						operationId: upload.operationId,
+						groupId: upload.groupId || null,
+						changes: upload.changes || {}, // ← ADD: Include changes
+						meta: {
+							originalName: upload.meta?.originalName || upload.originalFile?.name,
+							size: upload.meta?.size || upload.originalFile?.size,
+							type: upload.meta?.type || upload.originalFile?.type,
+							title: upload.meta?.title,
+							alt: upload.meta?.alt,
+							caption: upload.meta?.caption
+						}
+					};
 				}
+
+				// Return full upload object
 				return upload;
 			})
 			.filter(Boolean);
 	}
 
-	/**
-	 * Persist upload to DataStore
-	 */
-	async persistFieldState(fieldId) {
-		if (!this.db) return;
-
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Create clean field config without UI references
-		const { ui, container, dropZone, previewGrid, selectAll, selectActions, selectInfo, selectCount, groupDisplay, ...cleanConfig } = field;
-
-		const fieldState = {
-			fieldId: fieldId,
-			timestamp: Date.now(),
-
-			config: {
-				key: cleanConfig.key,
-				id: cleanConfig.id,
-				name: cleanConfig.name,
-				type: cleanConfig.type,
-				content: cleanConfig.content,
-				itemID: cleanConfig.itemID,
-				context: cleanConfig.context,
-				mode: cleanConfig.mode,
-				maxFiles: cleanConfig.maxFiles,
-				multiple: cleanConfig.multiple
-			},
-
-			// Recovery context
-			context: {
-				url: window.location.href,
-				modalType: this.getModalType(field),
-				formId: field.formId
-			},
-
-			// Uploads with their group associations (cleaned)
-			uploads: this.getFieldUploads(fieldId, true),
-
-			// Groups structure (ensure these are also cleaned)
-			groups: Array.from(this.groups.entries())
-				.filter(([id, data]) => data.fieldId === fieldId && data.uploads.size > 0)
-				.map(([id, data]) => ({
-					id: data.id,
-					uploads: Array.from(data.uploads),
-					meta: data.meta,
-					changes: data.changes
-				}))
-		};
-
-		const tx = this.db.transaction(['fieldStates'], 'readwrite');
-		await tx.objectStore('fieldStates').put(fieldState);
-	}
-	/*******************************************************************************
-	 RESTORE FUNCTIONALITY
-	*******************************************************************************/
-	async checkPendingUploads() {
+	async checkForStoredUploads() {
 		if (!this.db) return;
 
 		const tx = this.db.transaction(['fieldStates'], 'readonly');
@@ -2446,12 +2366,29 @@
 			request.onsuccess = () => resolve(request.result);
 		});
 
+		//
+		// allFieldStates.forEach(field => {
+		// 	console.log(`Field ${field.fieldId} has ${field.uploads.length} uploads:`);
+		// 	field.uploads.forEach((upload, idx) => {
+		// 		console.log(`  Upload ${idx}:`, {
+		// 			id: upload.id,
+		// 			status: upload.status,
+		// 			operationId: upload.operationId,
+		// 			hasOperationId: !!upload.operationId
+		// 		});
+		// 	});
+		// });
+
 		// Filter for pending uploads (not yet sent to server)
 		const pendingFields = allFieldStates.filter(field =>
 			field.uploads.some(upload =>
-				upload.status === 'processing' ||
-				upload.status === 'processed' ||
-				upload.status === 'pending'
+				// If no operationId, it hasn't been sent to server yet
+				!upload.operationId &&
+				// And it's been processed locally
+				(upload.status === 'completed' ||
+					upload.status === 'processed' ||
+					upload.status === 'local_processing' ||
+					upload.status === 'processed-original')
 			)
 		);
 
@@ -2461,132 +2398,359 @@
 		this.showRecoveryNotification(pendingFields);
 	}
 
-	showRecoveryNotification(pendingFields) {
-		const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
+	async handleRestoreUploads() {
+		let notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) {
+			return;
+		}
 
-		let notification = window.getTemplate('restoreNotification');
-		[
-			notification.querySelector('.restore-details').textContent,
-		] = [
-			`${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`
-		];
+		const selectedUploads = this.getSelectedRestorationUploads(notification);
+		if (selectedUploads.length === 0) {
+			return;
+		}
+		await this.restoreSelectedUploads(selectedUploads);
 
-		pendingFields.forEach(field => {
-			console.log(field);
-			let template = window.getTemplate('restoreField');
-			field.uploads.forEach(upload => {
-				let uploadItem = window.getTemplate('restoreItem');
-				[
-					uploadItem.querySelector('img').src
-				] = [
-					upload.preview
-				];
-				template.append(uploadItem);
-			});
-			notification.append(template);
-		});
-
-
-		// Add event handlers
-		notification.querySelector('[data-action="restore"]').addEventListener('click', () => {
-			this.restoreFieldStates(pendingFields);
-			notification.remove();
-		});
-
-		notification.querySelector('[data-action="dismiss"]').addEventListener('click', () => {
-			this.notifications.add('Uploads saved for later restoration', 'info');
-			notification.remove();
-		});
-
-		notification.querySelector('[data-action="clear"]').addEventListener('click', () => {
-			this.clearCachedUploads(pendingFields);
-			notification.remove();
-		});
-
-		document.body.appendChild(notification);
+		this.cleanupRestore();
 	}
 
-	async restoreFieldStates(fieldStates) {
-		// Group by URL
-		const byUrl = new Map();
-		fieldStates.forEach(field => {
-			if (!byUrl.has(field.context.url)) {
-				byUrl.set(field.context.url, []);
+	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
+				});
 			}
-			byUrl.get(field.context.url).push(field);
 		});
 
-		// If all on current page, restore directly
-		if (byUrl.size === 1 && byUrl.has(window.location.href)) {
-			for (const fieldState of fieldStates) {
+		return selected;
+	}
+
+	handleGroupMetaChange(input) {
+		let group = this.getGroupFromElement(input);
+		if (!group) {
+			return;
+		}
+		if (!Object.hasOwn(group, 'changes')) {
+			group.changes = {};
+		}
+		let name = input.name;
+		if (name.includes('group')) {
+			let replace = group.id+'_';
+			let replace2 = group.id+'[';
+			name = name.replace(replace, '').replace(replace2,'').replace(']', '');
+		}
+		group.changes[`${name}`] = input.value;
+		this.groups.set(group.id, group);
+		this.schedulePersistance(group.fieldId);
+	}
+
+
+	/*******************************************************************************
+	 * RESTORING UPLOADS
+	 *******************************************************************************/
+	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 (const upload of field.uploads) {
+
+				let uploadItem = window.getTemplate('uploadItem');
+				if (!uploadItem) continue;
+				//
+				// 	const imgEl = uploadItem.querySelector('img');
+				// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
+				//
+				const blobData = await this.uploadStore.getBlob(upload.id);
+
+
+				if (blobData) {
+					try {
+						// Create new blob URL from stored data
+						const blob = new Blob([blobData.data], { type: blobData.type });
+						const previewUrl = this.createPreviewUrl(blob);
+
+						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(blobData.type);
+						uploadItem.dataset.subtype = subtype;
+						switch (subtype) {
+							case 'image':
+								[
+									img.src,
+									img.alt
+								] = [
+									previewUrl,
+									upload.originalFile?.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 restoreSelectedUploads(selectedUploads) {
+		// Group by field
+		const byField = new Map();
+		selectedUploads.forEach(item => {
+			if (!byField.has(item.fieldId)) {
+				byField.set(item.fieldId, []);
+			}
+			byField.get(item.fieldId).push(item.uploadId);
+		});
+
+		// Get full field states from IndexedDB
+		if (!this.db) {
+			// this.notifications.add('Cannot restore: Database not available', 'error');
+			return;
+		}
+
+		const tx = this.db.transaction(['fieldStates'], 'readonly');
+		const store = tx.objectStore('fieldStates');
+
+		for (const [fieldId, uploadIds] of byField.entries()) {
+			const request = store.get(fieldId);
+			const fieldState = await new Promise(resolve => {
+				request.onsuccess = () => resolve(request.result);
+				request.onerror = () => resolve(null);
+			});
+
+			if (fieldState) {
+				// Filter to only selected uploads
+				fieldState.uploads = fieldState.uploads.filter(u => uploadIds.includes(u.id));
 				await this.restoreField(fieldState);
 			}
-			this.notifications.add(`Restored ${fieldStates.length} field(s)`, 'success');
-		} else {
-			// Store intent to restore and navigate
-			sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(fieldStates));
-
-			// Navigate to first URL
-			const firstUrl = byUrl.keys().next().value;
-			if (window.location.href !== firstUrl) {
-				window.location.href = firstUrl;
-			}
 		}
+
+		// this.notifications.add(`Restored ${selectedUploads.length} upload(s)`, 'success');
 	}
 
 	async restoreField(fieldState) {
-		const { config, context, uploads, groups } = fieldState;
+		const { config, context, uploads, groups, id } = fieldState;  // ← Use 'id'
 
 		// If in a modal, open it first
 		if (context.modalType) {
 			await this.openModalForRestore(context);
 		}
 
-		// Find the field element
-		const fieldElement = document.querySelector(
-			`.field.image[data-field-id="${config.id}"]`
-		);
+		// Find field element
+		let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
 
 		if (!fieldElement) {
-			console.warn(`Field ${config.id} not found for restoration`);
+			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
-		const fieldKey = this.registerUploader(fieldElement, config);
+		// Register the field if not already registered
+		let fieldKey = fieldElement.dataset.uploader;
+		if (!fieldKey || !this.fields.has(fieldKey)) {
+			fieldKey = this.registerUploader(fieldElement, config);
+		}
+
 		const field = this.fields.get(fieldKey);
+		if (!field) {
+			console.error('Failed to register field for restoration');
+			return;
+		}
+
+		// Merge saved state back into field
+		field.state = fieldState.state || 'ready';
+
+		// Rebuild UI references
+		field.ui = this.buildFieldUI(fieldElement);
+
+		if (field.ui.groups?.display) {
+			field.ui.groups.display.hidden = false;
+		}
+
+		// Restore groups
+		if (groups && groups.length > 0) {
+			await this.restoreGroups(fieldKey, groups);
+		}
 
 		// Restore uploads
 		for (const uploadData of uploads) {
 			await this.restoreUpload(field, uploadData);
 		}
 
-		// Restore groups
-		if (groups && groups.length > 0) {
-			await this.restoreGroups(field, groups, uploads);
-		}
-
 		// Update UI
+		this.updateFieldState(fieldKey);
 		this.maybeLockUploads(fieldKey);
 
 		// Queue for upload if needed
-		if (config.mode === 'direct') {
+		if (config.mode === 'direct' && config.destination !== 'post_group') {
 			await this.queueUpload(fieldKey);
 		}
 	}
 
 	async restoreUpload(field, uploadData) {
-		// Reconstruct the file from blob data
-		const blobData = await this.getBlobData(uploadData.id);
-		let file = null;
+		// Try to get blob data from IndexedDB
+		const blobData = await this.uploadStore.getBlob(uploadData.id);
 
 		if (blobData) {
-			file = new File(
-				[blobData.data],
-				blobData.name,
-				{ type: blobData.type, lastModified: blobData.lastModified }
-			);
+			const file = blobData.data instanceof File
+				? blobData.data
+				: new File(
+					[blobData.data],
+					blobData.name,
+					{ type: blobData.type, lastModified: blobData.lastModified }
+				);
+
+			uploadData.originalFile = file;
 			uploadData.processedFile = file;
+			uploadData.preview = this.createPreviewUrl(file);
+		} else {
+			console.warn('Blob data not found for upload:', uploadData.id);
+			return; // Skip this upload if we can't restore the file
 		}
 
 		// Add to field
@@ -2594,61 +2758,59 @@
 		field.uploads.add(uploadData.id);
 
 		// Recreate DOM element
-		uploadData.element = this.createImageElement(uploadData, field.type === 'groupable');
+		const subtype = this.getSubtypeFromMime(uploadData.originalFile.type);
+		uploadData.element = this.createUploadElement({
+			...uploadData,
+			subtype: subtype
+		}, field.config.destination === 'post_group');
 
 		// Restore to correct location
-		const location = uploadData.groupId
-			? field.ui.groups.groups.get(uploadData.groupId)
-			: field.ui.field.preview;
+		let location;
+		if (uploadData.groupId && field.ui.groups.groups.has(uploadData.groupId)) {
+			location = field.ui.groups.groups.get(uploadData.groupId).querySelector('.item-grid');
+		} else {
+			location = field.ui.preview;
+		}
 
 		if (location) {
-			location.append(uploadData.element);
+			location.appendChild(uploadData.element);
 			uploadData.location = location;
 		}
 
 		// Store in memory
 		this.uploads.set(uploadData.id, uploadData);
-	}
-
-	async restoreGroups(field, groups, uploads) {
-		for (const groupData of groups) {
-			// Create group element
-			const groupElement = this.createGroupElement(groupData.id, field.key);
-			field.ui.groups.groups.set(groupData.id, groupElement);
-			field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
-
-			// Create group Set
-			const groupSet = new Set(groupData.uploadIds);
-			this.groups.set(groupData.id, groupSet);
-
-			// Restore group metadata
-			if (groupData.meta) {
-				this.groupsMeta.set(groupData.id, groupData.meta);
-				// TODO: Populate meta fields in groupElement
+		if (uploadData.groupId) {
+			const group = this.groups.get(uploadData.groupId);
+			if (group && group.uploads) {
+				group.uploads.add(uploadData.id);
 			}
-
-			// Move uploads to group
-			groupData.uploadIds.forEach(uploadId => {
-				const upload = uploads.find(u => u.id === uploadId);
-				if (upload && upload.element) {
-					groupElement.querySelector('.item-grid').append(upload.element);
-					upload.location = groupElement.querySelector('.item-grid');
-					upload.groupId = groupData.id;
-				}
-			});
 		}
 	}
 
-	async getBlobData(uploadId) {
-		if (!this.db) return null;
+	async restoreGroups(fieldKey, groups) {
+		for (const groupData of groups) {
+			// Use createGroup which properly initializes EVERYTHING including selection handlers
+			const group = this.createGroup(fieldKey, groupData.id);
 
-		const tx = this.db.transaction(['uploadBlobs'], 'readonly');
-		const request = tx.objectStore('uploadBlobs').get(uploadId);
+			if (group) {
+				// Update the group metadata from saved state
+				if (groupData.meta) {
+					group.meta = { ...groupData.meta };
+				}
+				if (groupData.changes) {
+					group.changes = { ...groupData.changes };
+				}
 
-		return new Promise(resolve => {
-			request.onsuccess = () => resolve(request.result);
-			request.onerror = () => resolve(null);
-		});
+
+				// If you saved group titles, restore them
+				if (groupData.title) {
+					const titleInput = group.element.querySelector('[name*="post_title"]');
+					if (titleInput) {
+						titleInput.value = groupData.title;
+					}
+				}
+			}
+		}
 	}
 
 	async openModalForRestore(context) {
@@ -2678,370 +2840,68 @@
 		}
 	}
 
-	async clearCachedUploads(fieldStates) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
-
-		for (const field of fieldStates) {
-			// Delete field state
-			await tx.objectStore('fieldStates').delete(field.fieldId);
-
-			// Delete all associated blobs
-			for (const upload of field.uploads) {
-				await tx.objectStore('uploadBlobs').delete(upload.id);
-
-				// Clean up preview URLs
-				if (upload.preview?.startsWith('blob:')) {
-					URL.revokeObjectURL(upload.preview);
-				}
-			}
-		}
-
-		this.notifications.add('Cached uploads cleared', 'info');
-	}
-
-// Check for restoration intent on page load
-	async checkRestorationIntent() {
-		const restoreData = sessionStorage.getItem('jvb_restore_uploads');
-		if (!restoreData) return;
-
-		const fieldStates = JSON.parse(restoreData);
-		const currentUrlFields = fieldStates.filter(f => f.context.url === window.location.href);
-
-		if (currentUrlFields.length > 0) {
-			for (const fieldState of currentUrlFields) {
-				await this.restoreField(fieldState);
-			}
-
-			// Remove restored fields from session storage
-			const remaining = fieldStates.filter(f => f.context.url !== window.location.href);
-			if (remaining.length > 0) {
-				sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(remaining));
-			} else {
-				sessionStorage.removeItem('jvb_restore_uploads');
-			}
-
-			this.notifications.add(`Restored ${currentUrlFields.length} field(s)`, 'success');
-		}
-	}
 	/*******************************************************************************
-	 GROUP FUNCTIONALITY
-	 Includes selection, dragging, and grouping logic
-	*******************************************************************************/
-	/**
-	 *
-	 * @param {string} uploadId as defined by setUpload
-	 * @param {HTMLElement|null} target The target location
-	 * @param {boolean} persist whethet to cache this change
-	 */
-	addImageToGroup(uploadId, target = null, persist = true) {
-		let upload = this.getUpload(uploadId);
-		if(!upload) {
-			return;
-		}
-		let field = this.fields.get(upload.fieldId);
-		if (!field) {
-			return;
-		}
-		//Already in the Preview Grid, or already in the group we're moving to
-		if (!target && upload.location === field.ui.field.preview || target === upload.location) {
-			return;
-		}
+	 INDEXEDDB CACHE FUNCTIONALITY
+	 *******************************************************************************/
+	handleFieldStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
 
-		if (upload.location) {
-			let groupId = upload.location.dataset.groupId;
-			if (groupId) {
-				let group = this.groups.get(groupId);
-				if (group) {
-					group.delete(uploadId);
-					if (group.size === 0) {
-						this.removeGroup(groupId);
-					}
-				}
-			}
+				break;
+			case 'item-saved':
+				console.log(`Field state saved: ${data.key}`);
+				break;
 		}
+	}
 
-		upload.element.querySelector('[name="featured"]').hidden = !target;
-		//If no target, it's going to the preview grid
-		if (!target) {
-			target = field.ui.field.preview;
+	handleUploadStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				this.checkForStoredUploads();
+				break;
+			case 'item-saved':
+				this.showSaveIndicator(data.key);
+				break;
+		}
+	}
+	async saveUpload(upload) {
+		// Use the processed file if available, otherwise original
+		const fileToStore = upload.processedFile || upload.originalFile || upload.file;
+
+		if (fileToStore instanceof File || fileToStore instanceof Blob) {
+			await this.uploadStore.saveBlob(upload.id, fileToStore);
+
+			// Don't store file objects in main store
+			const { file, originalFile, processedFile, ...cleanUpload } = upload;
+			await this.uploadStore.save(cleanUpload);
 		} else {
-			let groupId = target.dataset.groupId;
-			let group = this.groups.get(groupId);
-			if (!group) {
-				group = this.createGroup(upload.fieldId);
+			await this.uploadStore.save(upload);
+		}
+	}
+
+	async loadFields() {
+		// Load all field states from the store
+		const fields = await this.fieldStore.getAll();
+
+		fields.forEach(field => {
+			// Reconstruct upload sets
+			if (field.uploads && Array.isArray(field.uploads)) {
+				field.uploads = new Set(field.uploads.map(u => u.id));
 			}
-			group.uploads.add(uploadId);
-		}
-
-
-		target.append(upload.element);
-		if (persist) {
-			this.persistFieldState(field.key);
-		}
-	}
-
-	addSelectionToGroup(target) {
-		let field = this.getFieldFromElement(target);
-		if (!field) {
-			return;
-		}
-		if (this.selected.get(field.key).size === 0) {
-			return;
-		}
-		let group = this.getGroupFromElement(target);
-		if (!group) {
-			group = this.createGroup(field.key);
-		}
-
-		Array.from(this.selected).forEach(uploadId => {
-			this.addImageToGroup(uploadId, group.grid, false);
-		});
-
-		this.persistFieldState(group.fieldId);
-	}
-
-
-	/**
-	 * Remove an empty group from the field
-	 * @param {string} groupId - The group to remove
-	 * @param {boolean} confirm - ask for confirmation
-	 */
-	removeGroup(groupId, confirm = false) {
-		let group = this.groups.get(groupId);
-		if (!group) {
-			return;
-		}
-		if (confirm) {
-			if(!window.confirm('This will delete this group. Any uploads in this group will return to the main grid. Are you sure?')){
-				return;
-			}
-		}
-
-		if (group.uploads.size > 0) {
-			Array.from(group.uploads).forEach(upload => {
-				this.addImageToGroup(upload);
-			});
-		}
-
-		let groupElement = group.element;
-		// Remove DOM element
-		if (groupElement) {
-			window.fade(groupElement, false);
-			this.a11y.announce('Empty group removed');
-		}
-
-		this.persistFieldState(group.fieldId);
-	}
-
-	createGroup(fieldId) {
-		let field = this.fields.get(fieldId);
-		if(!field) {
-			return;
-		}
-		let index = field.ui.groups.size;
-		field.ui.groups.groups.set(`group-${index}`, this.createGroupElement(`group-${index}`, fieldId));
-		let group = field.ui.groups.groups.get(`group-${index}`);
-		field.ui.groups.container.insertAfter(group, field.ui.groups.empty);
-		let groupConfig = {
-			fieldId: field.key,
-			id: `group-${index}`,
-			element: group,
-			grid: group.querySelector('.item-grid'),
-			uploads: new Set(),
-			meta: {
-				post_title: '',
-				post_excerpt: '',
-			},
-			changes: {},
-		};
-		this.groups.set(`group-${index}`, groupConfig);
-		return groupConfig;
-	}
-
-	createGroupElement(groupId, fieldId) {
-		let post = window.getTemplate('imageGroup');
-		if (!post) {
-			return;
-		}
-		post.dataset.groupId = groupId;
-		post.dataset.fieldId = fieldId;
-		let fields = window.getTemplate('groupMetaData');
-		post.querySelector('.fields')?.append(fields);
-
-		return post;
-	}
-
-	/**
-	 * Handle select all functionality
-	 */
-	handleSelectAll(element, checked = null) {
-		const field = this.getFieldFromElement(element);
-		if (!field) return;
-
-		// Use element's checked state if not provided
-		if (checked === null) {
-			checked = element.checked;
-		}
-
-		const target = field.previewGrid;
-		const previewItems = target.querySelectorAll('[data-upload-id]') || [];
-
-		previewItems.forEach(item => {
-			const checkbox = item.querySelector('[name*="select-item"]');
-			if (checkbox) {
-				checkbox.checked = checked;
-			}
-		});
-
-		this.updateSelectAll(element);
-		this.a11y.announce(checked ? 'All uploads selected' : 'All uploads deselected');
-
-		// Clear last clicked since we're selecting/deselecting all
-		this.lastClickedUpload = null;
-	}
-
-	updateSelection(e) {
-		let field = this.getFieldFromElement(e.target);
-		let upload = this.getUploadFromElement(e.target);
-		if (!field || ! upload) {
-			console.log('No field or upload found...');
-			return;
-		}
-
-		this.lastClickedUpload = upload.id;
-		let action = e.target.checked;
-		if (action) {
-			this.selected.get(field.key).add(upload.id);
-		} else {
-			this.selected.get(field.key).delete(upload.id);
-		}
-	}
-
-	updateSelectAll(element) {
-		const field = this.getFieldFromElement(element);
-		if (!field) return;
-
-		const selected = this.getSelectedUploads(element);
-		if (selected.length > 0 ) {
-			field.selectActions.hidden = false;
-			field.selectInfo.hidden = false;
-			field.selectCount.textContent = `${selected.length}`;
-		} else {
-			field.selectActions.hidden = true;
-			field.selectInfo.hidden = true;
-		}
-		let selectAll = selected.length === field.container.querySelectorAll('.item-grid.preview .upload-item').length;
-		field.selectAll.checked = selectAll;
-		field.selectAll.nextElementSibling.textContent = (selectAll) ? 'Clear Selection' : 'Select All';
-	}
-
-	getSelectedUploads(element) {
-		let field = this.getFieldFromElement(element);
-		if (!field) {
-			return;
-		}
-		return Array.from(this.selected.get(field.key)??[]);
-	}
-
-
-	handleRangeSelection(currentElement, event) {
-		const field = this.getFieldFromElement(currentElement);
-		if (!field) return;
-
-		const currentUploadId = this.getUploadIdFromElement(currentElement);
-		if (!currentUploadId || !this.lastClickedUpload) return;
-
-		// Get all upload items in the preview grid
-		const container = currentElement.closest('.item-grid');
-		const allItems = Array.from(container.querySelectorAll('[data-upload-id]'));
-
-		// Find indices of first and current items
-		const firstIndex = allItems.findIndex(item =>
-			item.dataset.uploadId === this.lastClickedUpload
-		);
-		const currentIndex = allItems.findIndex(item =>
-			item.dataset.uploadId === currentUploadId
-		);
-
-		if (firstIndex === -1 || currentIndex === -1) return;
-
-		// Determine range (handle both directions)
-		const startIndex = Math.min(firstIndex, currentIndex);
-		const endIndex = Math.max(firstIndex, currentIndex);
-
-		// Select all items in range (including the clicked one!)
-		for (let i = startIndex; i <= endIndex; i++) {
-			const item = allItems[i];
-			const checkbox = item.querySelector('[name*="select-item"]');
-			if (checkbox) {
-				checkbox.checked = true;
-			}
-		}
-
-		currentElement.checked = true;
-		// Update selection UI
-		this.updateSelectAll(currentElement);
-
-		// Announce the range selection
-		const selectedCount = endIndex - startIndex + 1;
-		this.a11y.announce(`Selected ${selectedCount} items in range`);
-
-		// Update the last clicked item to the current one
-		this.lastClickedUpload = currentUploadId;
-	}
-
-	removeSelection(button) {
-		let fieldId = this.getFieldIdFromElement(button);
-
-		const selectedUploads = this.getSelectedUploads(button);
-		if (selectedUploads.length === 0) {
-			this.notify('No uploads selected', 'warning');
-			return;
-		}
-
-		selectedUploads.forEach(upload => {
-			this.removeUpload(fieldId, upload);
+			this.fields.set(field.fieldId, field);
 		});
 	}
 
-	removeUpload(fieldId, uploadId) {
-		const field = this.fields.get(fieldId);
-		const upload = this.uploads.get(uploadId);
-
-		if (!field || !upload) return;
-
-		// Remove from field
-		field.uploads?.delete(uploadId);
-
-		// Remove from group if grouped
-		if (upload.groupId) {
-			const group = this.groups.get(upload.groupId);
-			group?.delete(uploadId);
-		}
-
-		// Clean up element
-		upload.element?.remove();
-
-		// Clean up memory
-		this.clearUpload(uploadId);
-
-		// Update UI
-		this.maybeLockUploads(fieldId);
-		this.updateSelectAll(field.ui.field.field);
-
-		this.a11y.announce('Upload removed');
+	async loadUploads() {
+		const uploads = await this.uploadStore.getAll();
+		uploads.forEach(upload => {
+			this.uploads.set(upload.id, upload);
+		});
 	}
 
 	/**************************************************************************
-	 META
-	 Handled separately, in case it is edited in the middle of processing images
-	**************************************************************************/
-
-	/**************************************************************************
 	 SUBSCRIBERS
-	**************************************************************************/
+	 **************************************************************************/
 	/**
 	 * Event system
 	 */
@@ -3053,35 +2913,111 @@
 	notify(event, data) {
 		this.subscribers.forEach(cb => cb(event, data));
 	}
+	/*******************************************************************************
+	 * CLEANUP
+	 *******************************************************************************/
 
-	handleBeforeUnload(e) {
-		// Check for any uploads in processing or pending state
-		const unsavedUploads = Array.from(this.uploads.values()).filter(upload =>
-			upload.status === 'processing' ||
-			upload.status === 'pending' ||
-			upload.status === 'uploading'
-		);
+	destroy() {
+		// Remove core listeners
+		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 (unsavedUploads.length > 0) {
-			const message = 'You have uploads in progress. Are you sure you want to leave?';
-			e.preventDefault();
-			e.returnValue = message;
-			return message;
+		// Destroy drag controller
+		if (this.dragController) {
+			this.dragController.destroy();
 		}
-	}
-	/**************************************************************************
-	 CLEANUP
-	**************************************************************************/
-	cleanup() {
-		this.clearListeners();
-		if (this.hasGroups) {
-			this.clearGroupListeners();
-		}
-		this.compressionWorker = null;
+
+		// Destroy selection handlers
+		this.selectionHandlers.forEach(handler => handler.destroy());
+		this.selectionHandlers.clear();
+
+		this.cleanupAllPreviewUrls();
+
+		// Clear data
+		this.fields.clear();
+		this.uploads.clear();
+		this.groups.clear();
+		this.selected.clear();
 		this.subscribers.clear();
 	}
+
+	cleanupRestore() {
+		this.restoreModal.handleClose();
+		this.restoreSelection.destroy();
+		this.restoreSelection = null;
+		this.restoreModal.destroy();
+		this.restoreModal.modal.remove();
+		this.restoreModal = null;
+	}
+
+	async cleanupStoredUploads() {
+		this.fieldStore.clear();
+		this.uploadStore.clear();
+	}
+
+	/**
+	 * Clear all uploads for a field and cleanup resources
+	 */
+	async clearField(fieldId) {
+		// Clear from stores
+		await this.fieldStore.delete(fieldId);
+
+		// Clear related uploads
+		const field = this.fields.get(fieldId);
+		if (field?.uploads) {
+			for (const uploadId of field.uploads) {
+				await this.uploadStore.delete(uploadId);
+			}
+		}
+
+		// Clear from memory
+		this.fields.delete(fieldId);
+	}
+
+	async clearUpload(uploadId, persist = true) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload) return;
+
+		// Clean up preview URL using helper
+		this.revokePreviewUrl(upload.preview);
+
+		// Clean up element preview URL
+		if (upload.element) {
+			const previewUrl = upload.element.dataset.previewUrl;
+			this.revokePreviewUrl(previewUrl);
+			delete upload.element.dataset.previewUrl;
+		}
+
+		if (persist) {
+			await this.schedulePersistance(upload.fieldId);
+		}
+
+		// Remove from memory
+		this.uploads.delete(uploadId);
+
+		// Remove from IndexedDB
+		this.uploadStore.delete(uploadId);
+		this.uploadStore.delete(uploadId, 'blobs');
+	}
+	cleanupAllPreviewUrls() {
+		if (this.previewUrls) {
+			this.previewUrls.forEach(url => {
+				try {
+					URL.revokeObjectURL(url);
+				} catch (e) {
+					// Ignore errors during cleanup
+				}
+			});
+			this.previewUrls.clear();
+		}
+	}
 }
 
+// Initialize when DOM is ready
 document.addEventListener('DOMContentLoaded', () => {
 	window.jvbUploads = new UploadManager();
 });

--
Gitblit v1.10.0