From 0e4b986e81f8132a44e61fa8df18860301cc3468 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 01 Jan 2026 20:31:10 +0000
Subject: [PATCH] =JakeVan preliminary additions

---
 assets/js/concise/FormController.js | 1829 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 1,641 insertions(+), 188 deletions(-)

diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index eea0597..a0388e2 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -1,13 +1,27 @@
-/**
- * Enhanced FormController - Manages forms with special fields, caching, and queue integration
- * Works with DataStore for CRUD operations and standalone for front-end forms
- */
 class FormController {
-	constructor(store = null) {
-		this.store = store; // Optional - for CRUD operations
-		if (!store) {
-			this.store = new window.jvbStore({name:'forms', TTL: 604800});
+	constructor(config = {}) {
+		this.config = {
+			collectFormData: false,
+			... config
 		}
+		this.isRestoring = false;
+		const store = window.jvbStore.register(
+			'forms',
+			{
+				storeName: 'forms',
+				keyPath: 'formId',
+				indexes: [
+					{ name: 'status', keyPath: 'status' },
+					{ name: 'operationId', keyPath: 'operationId' },
+					{ name: 'timestamp', keyPath: 'timestamp' },
+					{ name: 'formType', keyPath: 'type' }
+				],
+				TTL: 7 * 24 * 60 * 1000, //7 days
+				validateData: true,
+				delayFetch: true
+			});
+		this.store = store.forms;
+
 		this.debouncer = window.debouncer;
 
 		this.ignore = [];
@@ -19,6 +33,10 @@
 		this.specialFields = new Map();
 		this.dependencies = new Map();
 
+		// Validation (YOU ARE GREAT!)
+		this.validators = this.initValidators();
+		this.touchedFields = new Set();
+
 		// Auto-save configuration
 		this.autoSaveDefaults = {
 			delay: 3000, // 3 seconds
@@ -37,63 +55,164 @@
 			reorder: 1000
 		};
 
+		this.isTimeline = window.crudManager && window.crudManager.isTimeline;
+
 		// Bind handlers
 		this.clickHandler = this.handleClick.bind(this);
 		this.changeHandler = this.handleChange.bind(this);
 		this.submitHandler = this.handleSubmit.bind(this);
 		this.inputHandler = this.handleInput.bind(this);
-		this.focusHandler = this.handleFocus.bind(this);
 		this.blurHandler = this.handleBlur.bind(this);
+		//Processors
+		this.processRepeaterField = this.processRepeaterField.bind(this);
+		this.processGroupField = this.processGroupField.bind(this);
+		this.processLocationField = this.processLocationField.bind(this);
+		this.processRegularField = this.processRegularField.bind(this);
 
 		this.init();
 	}
 
 	async init() {
-		// Check for pending operations on page load
-		await this.checkPendingOperations();
+		this.store.subscribe(this.handleStoreEvent.bind(this));
 
 		// Set up global form handlers for standalone forms
 		this.initListeners();
+		if (window.jvbQueue) {
+			window.jvbQueue.subscribe((event, data) => {
+				if (event === 'operation-completed' && data.type === 'form') {
+					this.handleOperationComplete(data);
+				}
+			});
+		}
 	}
 
 	/**
-	 * Check for pending operations from previous session
+	 * Handle operation completion - clear related form cache
 	 */
-	async checkPendingOperations() {
-		if (!this.store) return;
-		try {
-			let pending = this.store.getAllForms();
-
-		} catch (error) {
-			console.error('Failed to load pending forms:', error);
+	async handleOperationComplete(operation) {
+		// Clear the form data from store
+		if (operation.formId) {
+			try {
+				await this.store.delete(operation.formId);
+			} catch (error) {
+				console.warn('Failed to clear form cache:', error);
+			}
 		}
+
+		// Clear any related form state
+		const form = this.forms.get(operation.formId);
+		if (form) {
+			form.isDirty = false;
+			form.lastSaved = Date.now();
+			form.data = {};
+		}
+	}
+
+	handleStoreEvent(event, data) {
+		switch(event) {
+			case 'item-saved':
+				if (data.item.status === 'autosave') {
+					// this.showFormStatus(data.item.formId, 'autosave');
+				}
+				break;
+			case 'data-loaded':
+				this.checkPendingForms();
+				break;
+		}
+	}
+
+	/**
+	 * Check for pending forms from current page
+	 */
+	async checkPendingForms() {
+		const allForms = await this.store.getAll();
+		const currentPath = window.location.pathname;
+
+		const pendingForms = allForms.filter(form => {
+			if (form.status !== 'draft') return false;
+
+			// Check if form is from current page
+			const formPath = form.data?._wp_http_referer;
+			return formPath === currentPath;
+		});
+
+		pendingForms.forEach(item => {
+			const formElement = this.findFormElement(item);
+			if (!formElement) return;
+
+			// Register form if not already registered
+			let formConfig = this.forms.get(item.formId);
+			if (!formElement.dataset.formId) {
+				formConfig = this.registerForm(formElement);
+			}
+
+			// Set flag to prevent event handlers from firing
+			this.isRestoring = true;
+			// Auto-populate the form
+			new this.populateForm(formElement, item.data);
+
+			// Reset flag after a tick (gives DOM time to settle)
+			setTimeout(() => {
+				this.isRestoring = false;
+			}, 0);
+
+			// Show restore status
+			this.showFormStatus(item.formId, 'restored');
+
+			if (window.jvbA11y) {
+				window.jvbA11y.announce('Your previous entry has been restored');
+			}
+		});
+	}
+
+	/**
+	 * Find form element that matches the cached data
+	 */
+	findFormElement(formData) {
+		// Try by form_id first (hidden field)
+		if (formData.data?.form_id) {
+			const form = document.querySelector(`[name="form_id"][value="${formData.data.form_id}"]`)?.closest('form');
+			if (form) return form;
+		}
+
+		// Try by form_type
+		if (formData.data?.form_type) {
+			const form = document.querySelector(`[name="form_type"][value="${formData.data.form_type}"]`)?.closest('form');
+			if (form) return form;
+		}
+
+		// Fallback: try by formId (if it was already registered)
+		return document.querySelector(`[data-form-id="${formData.formId}"]`);
 	}
 
 	/**
 	 * Show notification for pending changes
 	 */
-	showPendingNotification(pendingData) {
-		const formElement = document.querySelector(`[data-form-id="${pendingData.formId}"]`);
+	/**
+	 * Show notification for pending changes
+	 */
+	showPendingNotification(formId, formData) {
+		const formElement = document.querySelector(`[data-form-id="${formId}"]`);
 		if (!formElement) return;
 
 		const notification = document.createElement('div');
 		notification.className = 'pending-changes-notification';
 		notification.innerHTML = `
-			<p>We noticed unsaved changes from last time. Would you like to restore them?</p>
-			<button class="restore-changes" data-form-id="${pendingData.formId}">Restore</button>
-			<button class="discard-changes" data-form-id="${pendingData.formId}">Discard</button>
-		`;
+        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>
+        <button class="restore-changes" data-form-id="${formId}">Restore</button>
+        <button class="discard-changes" data-form-id="${formId}">Discard</button>
+    `;
 
 		formElement.insertBefore(notification, formElement.firstChild);
 
 		// Add handlers
-		notification.querySelector('.restore-changes').addEventListener('click', () => {
-			this.restorePendingForm(pendingData);
+		notification.querySelector('.restore-changes').addEventListener('click', async () => {
+			await this.restorePendingForm(formId, formData);
 			notification.remove();
 		});
 
-		notification.querySelector('.discard-changes').addEventListener('click', () => {
-			this.discardPendingForm(pendingData.formId);
+		notification.querySelector('.discard-changes').addEventListener('click', async () => {
+			await this.discardPendingForm(formId);
 			notification.remove();
 		});
 	}
@@ -101,16 +220,20 @@
 	/**
 	 * Restore pending form data
 	 */
-	restorePendingForm(pendingData) {
-		const form = document.querySelector(`[data-form-id="${pendingData.formId}"]`);
+	async restorePendingForm(formId, formData) {
+		const form = document.querySelector(`[data-form-id="${formId}"]`);
 		if (!form) return;
 
 		// Populate form with cached data
-		new this.populateForm(form, pendingData.formData);
+		new this.populateForm(form, formData);
 
-		// Mark as restored
-		pendingData.status = 'restored';
-		this.pendingForms.set(pendingData.formId, pendingData);
+		// Update status in store (mark as restored, not draft)
+		await this.store.save({
+			formId: formId,
+			data: formData,
+			status: 'restored',
+			timestamp: Date.now()
+		});
 
 		if (window.jvbA11y) {
 			window.jvbA11y.announce('Previous changes restored');
@@ -121,10 +244,14 @@
 	 * Discard pending form data
 	 */
 	async discardPendingForm(formId) {
-		this.store.clearForm(formId);
+		try {
+			await this.store.delete(formId);
 
-		if (window.jvbA11y) {
-			window.jvbA11y.announce('Previous changes discarded');
+			if (window.jvbA11y) {
+				window.jvbA11y.announce('Previous changes discarded');
+			}
+		} catch (error) {
+			console.error('Failed to discard pending form:', error);
 		}
 	}
 
@@ -134,11 +261,10 @@
 	initListeners() {
 		// Only add if not already added
 		if (!this.globalHandlersAdded) {
-			document.addEventListener('submit', this.submitHandler);
 			document.addEventListener('click', this.clickHandler);
 			document.addEventListener('change', this.changeHandler);
-			document.addEventListener('focus', this.focusHandler, true);
 			document.addEventListener('blur', this.blurHandler, true);
+			document.addEventListener('input', this.inputHandler);
 			this.globalHandlersAdded = true;
 		}
 	}
@@ -147,35 +273,37 @@
 	 * Register a standalone form (for front-end forms)
 	 */
 	registerForm(formElement, options = {}) {
+		if (!formElement) return;
 		const formId = formElement.dataset.formId || `form_${Date.now()}`;
 		formElement.dataset.formId = formId;
 
+		formElement.addEventListener('submit', this.submitHandler);
+
 		const formConfig = {
 			element: formElement,
 			id: formId,
+			status: '',
 			options: {
-				autoSave: true,
+				autosave: 'autosave' in formElement.dataset,
+				autoUpload: true,
 				saveDelay: this.autoSaveDefaults.delay,
-				endpoint: formElement.dataset.save,
+				endpoint: formElement.dataset.save ?? '',
+				formStatus: true,
 				cache: true,
 				...options
 			},
 			dependencies: new Map(),
-			data: this.collectFormData(formElement),
-			isDirty: false
+			data: this.collectFormData(formElement, true),
 		};
 
-		// Initialize special fields
 		this.initializeFormFields(formElement, formConfig);
-
-		// Store form config
 		this.forms.set(formId, formConfig);
 
-		// Check for pending data
+		// Check for pending data - FIXED
 		if (this.store && formConfig.options.cache) {
-			const cached = this.store.getForm(formId);
-			if (cached && cached.formData) {
-				this.showPendingNotification(cached);
+			const cached = this.store.get(formId);
+			if (cached && cached.data) {
+				this.showPendingNotification(formId, cached.data);
 			}
 		}
 
@@ -192,6 +320,8 @@
 		// Initialize repeater fields
 		this.initRepeaterFields(form, formConfig);
 
+		this.initTagListFields(form, formConfig);
+
 		// Initialize conditional fields
 		if (formConfig) {
 			this.initConditionalFields(form, formConfig);
@@ -201,20 +331,136 @@
 		this.initCharacterLimits(form);
 
 		// Initialize image upload fields
-		this.initImageUploadFields(form);
+		this.initImageUploadFields(form, formConfig);
 
 		// Initialize tabs if present
 		if (window.jvbTabs && form.querySelector('nav.tabs')) {
-			new window.jvbTabs(form);
+			formConfig.tabs = new window.jvbTabs(form);
+			this.forms.set(formConfig.formId, formConfig);
+			this.initSteppedForm(formConfig.formId);
 		}
 
 		// Scan for existing selector fields
 		if (window.jvbSelector) {
-			window.jvbSelector.scanExistingFields();
+			window.jvbSelector.scanExistingFields(form);
 		}
 	}
 
 	/**
+	 * Initialize stepped form functionality
+	 */
+	initSteppedForm(formId) {
+		const formConfig = this.forms.get(formId);
+		const form = formConfig.element;
+		const tabsInstance = formConfig.tabs;
+
+		const sections = form.querySelectorAll('.tab-content');
+		const totalSteps = sections.length;
+		const progressBar = form.querySelector('.form-progress .fill');
+		const stepText = form.querySelector('.step-text .current');
+		const tabButtons = form.querySelectorAll('nav.tabs button');
+
+		// Update progress display
+		const updateProgress = (currentStep) => {
+			const progress = (currentStep / totalSteps) * 100;
+			if (progressBar) {
+				progressBar.style.width = progress + '%';
+			}
+			if (stepText) {
+				stepText.textContent = currentStep;
+			}
+
+			// Update tab states
+			tabButtons.forEach((btn, idx) => {
+				const stepNum = idx + 1;
+				btn.classList.remove('current', 'completed', 'pending');
+
+				if (stepNum < currentStep) {
+					btn.classList.add('completed');
+				} else if (stepNum === currentStep) {
+					btn.classList.add('current');
+				} else {
+					btn.classList.add('pending');
+				}
+			});
+		};
+
+		// Next/Previous button handling
+		form.addEventListener('click', (e) => {
+			const nextBtn = e.target.closest('[data-action="next-step"]');
+			const prevBtn = e.target.closest('[data-action="prev-step"]');
+
+			if (nextBtn) {
+				e.preventDefault();
+				const currentSection = nextBtn.closest('.tab-content');
+				const currentStep = parseInt(currentSection.dataset.step);
+				const nextSection = form.querySelector(`.tab-content[data-step="${currentStep + 1}"]`);
+
+				if (nextSection && this.validateStep(currentSection)) {
+					const nextTab = nextSection.dataset.tab;
+					tabsInstance.switchTab(nextTab, true);
+					updateProgress(currentStep + 1);
+
+					// Scroll to top of form
+					form.scrollIntoView({ behavior: 'smooth', block: 'start' });
+				}
+			}
+
+			if (prevBtn) {
+				e.preventDefault();
+				const currentSection = prevBtn.closest('.tab-content');
+				const currentStep = parseInt(currentSection.dataset.step);
+				const prevSection = form.querySelector(`.tab-content[data-step="${currentStep - 1}"]`);
+
+				if (prevSection) {
+					const prevTab = prevSection.dataset.tab;
+					tabsInstance.switchTab(prevTab, true);
+					updateProgress(currentStep - 1);
+
+					// Scroll to top of form
+					form.scrollIntoView({ behavior: 'smooth', block: 'start' });
+				}
+			}
+		});
+
+		// Update progress when tabs are clicked directly
+		const originalSwitchTab = tabsInstance.switchTab.bind(tabsInstance);
+		tabsInstance.switchTab = (tab, updateHistory) => {
+			originalSwitchTab(tab, updateHistory);
+			const activeSection = form.querySelector(`.tab-content[data-tab="${tab}"]`);
+			if (activeSection) {
+				const step = parseInt(activeSection.dataset.step);
+				updateProgress(step);
+			}
+		};
+
+		// Initialize progress
+		updateProgress(1);
+	}
+
+	/**
+	 * Validate current step before allowing progression
+	 * Can be enhanced with custom validation rules
+	 */
+	validateStep(section) {
+		const fields = section.querySelectorAll('.field');
+		let allValid = true;
+
+		fields.forEach(fieldWrapper => {
+			const input = fieldWrapper.querySelector('input, textarea, select');
+			if (input && !input.closest('[hidden]')) {
+				const isValid = this.validateField(input, fieldWrapper);
+				if (!isValid) {
+					allValid = false;
+				}
+			}
+		});
+
+		return allValid;
+	}
+
+
+	/**
 	 * Initialize Quill editors
 	 */
 	initQuillEditors(form) {
@@ -285,8 +531,7 @@
 
 		container.appendChild(row);
 
-		// Schedule save if auto-save enabled
-		if (formConfig && formConfig.options.autoSave) {
+		if (formConfig) {
 			this.scheduleSave(formConfig, {
 				type: 'repeater',
 				action: 'add',
@@ -313,7 +558,7 @@
 		this.updateRepeaterOrder(repeater, formConfig);
 
 		// Schedule save
-		if (formConfig && formConfig.options.autoSave) {
+		if (formConfig) {
 			this.scheduleSave(formConfig, {
 				type: 'repeater',
 				action: 'remove',
@@ -356,7 +601,7 @@
 		});
 
 		// Schedule save
-		if (formConfig && formConfig.options.autoSave) {
+		if (formConfig) {
 			this.scheduleSave(formConfig, {
 				type: 'repeater',
 				action: 'reorder',
@@ -367,6 +612,231 @@
 	}
 
 	/**
+	 * Initialize tag list fields
+	 */
+	initTagListFields(form, formConfig) {
+		form.querySelectorAll('.field.tag-list').forEach(field => {
+			const inputRow = field.querySelector('.tag-input-row');
+			const addButton = field.querySelector('.add-tag-item');
+			const tagsContainer = field.querySelector('.tag-items');
+			const template = field.querySelector('.tag-template');
+			const fieldName = field.dataset.field;
+			const tagFormat = field.dataset.tagFormat || 'first_field';
+
+			if (!inputRow || !addButton || !tagsContainer || !template) return;
+
+			// Get all input fields in the input row (excluding the button)
+			const getInputFields = () => {
+				return Array.from(inputRow.querySelectorAll('input, select, textarea'))
+					.filter(input => !input.closest('button'));
+			};
+
+			// Add tag handler
+			const addTag = () => {
+				const inputs = getInputFields();
+				const data = {};
+				let hasValue = false;
+
+				// Collect values from inputs
+				inputs.forEach(input => {
+					const fieldName = input.name.replace('new_', '');
+					const value = this.getFieldValue(input);
+
+					if (value) hasValue = true;
+					data[fieldName] = value;
+				});
+
+				if (!hasValue) {
+					if (window.jvbA11y) {
+						window.jvbA11y.announce('Please fill in at least one field', 'error');
+					}
+					inputs[0].focus();
+					return;
+				}
+
+				// Validate required fields using data-required attribute
+				const invalidField = inputs.find(input => {
+					const isRequired = ('required' in input.dataset && input.dataset.required === '1');
+					const value = this.getFieldValue(input);
+					return isRequired && !value;
+				});
+
+				if (invalidField) {
+					const fieldWrapper = invalidField.closest('.field');
+					const fieldLabel = fieldWrapper?.querySelector('label')?.textContent || 'This field';
+					this.showError(fieldWrapper, `${fieldLabel} is required.`);
+
+					invalidField.focus();
+					return;
+				}
+
+				for (let input of inputs) {
+					let wrapper = field.closest('.field');
+					if (!this.validateField(input, wrapper)){
+						input.focus();
+						return;
+					}
+				}
+
+				// Clone template and populate
+				const index = tagsContainer.children.length;
+				const newTag = template.content.cloneNode(true).firstElementChild;
+				newTag.dataset.index = index;
+
+				// Update tag label
+				const tagLabel = newTag.querySelector('.tag-label');
+				if (tagLabel) {
+					tagLabel.textContent = this.getTagDisplayText(data, tagFormat);
+				}
+
+				// Update hidden inputs
+				newTag.querySelectorAll('input[type="hidden"]').forEach(input => {
+					const fieldKey = input.dataset.field;
+					input.name = `${fieldName}:${index}:${fieldKey}`;
+					input.value = data[fieldKey] || '';
+				});
+
+				tagsContainer.appendChild(newTag);
+
+				// Clear inputs
+				inputs.forEach(input => {
+					if (input.type === 'checkbox' || input.type === 'radio') {
+						input.checked = false;
+					} else {
+						input.value = '';
+					}
+					let field = input.closest('.field');
+					this.clearValidation(field);
+				});
+
+				// Focus first input
+				if (inputs.length > 0) {
+					inputs[0].focus();
+				}
+
+
+				// Schedule save
+				if (formConfig) {
+					this.scheduleSave(formConfig, {
+						type: 'tag_list',
+						action: 'add',
+						fieldName: fieldName,
+						delay: this.autoSaveDefaults.delay
+					});
+				}
+
+				if (window.jvbA11y) {
+					window.jvbA11y.announce('Item added');
+				}
+			};
+
+			// Add button click
+			addButton.addEventListener('click', addTag);
+
+			// Enter key support on last input
+			const inputs = getInputFields();
+			if (inputs.length > 0) {
+				// Tab through inputs, Enter on last one adds the tag
+				inputs[inputs.length - 1].addEventListener('keypress', (e) => {
+					if (e.key === 'Enter') {
+						e.preventDefault();
+						addTag();
+					}
+				});
+
+				// Enter on other inputs moves to next field
+				inputs.slice(0, -1).forEach((input, i) => {
+					input.addEventListener('keypress', (e) => {
+						if (e.key === 'Enter') {
+							e.preventDefault();
+							inputs[i + 1].focus();
+						}
+					});
+				});
+			}
+
+			// Remove tag handler
+			tagsContainer.addEventListener('click', (e) => {
+				if (e.target.closest('.remove-tag')) {
+					const tag = e.target.closest('.tag-item');
+					const tagText = tag.querySelector('.tag-label')?.textContent || 'Item';
+
+					tag.remove();
+
+					// Reindex remaining tags
+					this.reindexTagList(tagsContainer, fieldName);
+
+					// Schedule save
+					if (formConfig) {
+						this.scheduleSave(formConfig, {
+							type: 'tag_list',
+							action: 'remove',
+							fieldName: fieldName,
+							delay: this.autoSaveDefaults.delay
+						});
+					}
+
+					if (window.jvbA11y) {
+						window.jvbA11y.announce(`${tagText} removed`);
+					}
+				}
+			});
+		});
+	}
+
+	/**
+	 * Reindex tag list items
+	 */
+	reindexTagList(container, baseFieldName) {
+		Array.from(container.children).forEach((tag, index) => {
+			tag.dataset.index = index;
+
+			tag.querySelectorAll('input[type="hidden"]').forEach(input => {
+				const fieldKey = input.dataset.field;
+				input.name = `${baseFieldName}:${index}:${fieldKey}`;
+			});
+		});
+	}
+
+	/**
+	 * Get display text for tag based on format
+	 */
+	getTagDisplayText(data, format) {
+		const values = Object.values(data).filter(v => v);
+
+		if (values.length === 0) return 'New Item';
+
+		switch (format) {
+			case 'first_field':
+				return values[0];
+
+			case 'all_fields':
+				return values.join(', ');
+
+			default:
+				// Template format like "{name} ({email})"
+				if (format.includes('{')) {
+					let text = format;
+					for (const [key, value] of Object.entries(data)) {
+						text = text.replace(`{${key}}`, value);
+					}
+					return text;
+				}
+				// Use specific field
+				return data[format] || values[0];
+		}
+	}
+
+	/**
+	 * HTML escape helper
+	 */
+	escapeHtml(text) {
+		const div = document.createElement('div');
+		div.textContent = text;
+		return div.innerHTML;
+	}
+
+	/**
 	 * Initialize conditional fields
 	 */
 	initConditionalFields(form, formConfig) {
@@ -411,8 +881,8 @@
 		const requiredStr = String(requiredValue || '');
 
 		switch (operator) {
-			case '==': return fieldStr == requiredStr;
-			case '!=': return fieldStr != requiredStr;
+			case '==': return fieldStr === requiredStr;
+			case '!=': return fieldStr !== requiredStr;
 			case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
 			case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
 			case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
@@ -420,7 +890,7 @@
 			case 'contains': return fieldStr.includes(requiredStr);
 			case 'empty': return fieldStr === '';
 			case 'not_empty': return fieldStr !== '';
-			default: return fieldStr == requiredStr;
+			default: return fieldStr === requiredStr;
 		}
 	}
 
@@ -485,40 +955,202 @@
 	/**
 	 * Initialize image upload fields
 	 */
-	initImageUploadFields() {
-		window.jvbUploads.scanFields();
+	initImageUploadFields(form, config) {
+		window.jvbUploads.scanFields(form, config.options.autoUpload);
 	}
 
 	/* ========== Event Handlers ========== */
 
-	handleSubmit(event) {
-		if (this.subscribers.size > 0 ){
-			const form = event.target;
-			if (!form.dataset.formId) return;
+	async handleSubmit(event) {
+		const form = event.target;
 
+		if (!form.dataset.formId) return;
+		const formConfig = this.forms.get(form.dataset.formId);
+
+		// Handle subscriber-based forms
+		if (this.subscribers.size > 0) {
 			event.preventDefault();
-
-			const formConfig = this.forms.get(form.dataset.formId);
-			if (!formConfig) return;
-
 			const formData = this.collectFormData(form);
 
-			event.preventDefault();
+			// Notify subscribers (they'll handle actual submission)
 			this.notify('form-submit', {
-				formId: formConfig.id,
-				data: formData,
+				formId: form.dataset.formId,
+				fullData: formData,
 				config: formConfig
 			});
 		}
 	}
 
+	handleFormSuccess(form, data) {
+		// Clear previous errors
+		form.querySelectorAll('.error-message').forEach(el => el.remove());
+		form.querySelectorAll('.field-error').forEach(el =>
+			el.classList.remove('field-error')
+		);
+
+		// Add success class to form
+		form.classList.add('form-success');
+
+		// Show success message if provided
+		if (data.message) {
+			const success = document.createElement('div');
+			success.className = 'form-success-message success-message';
+			success.textContent = data.message;
+			form.insertBefore(success, form.firstChild);
+
+			const icon = window.getIcon?.('check-circle');
+			if (icon) {
+				icon.classList.add('success-icon');
+				success.prepend(icon);
+			}
+		}
+
+		// If there's a title/description (for registration success)
+		if (data.title || data.description) {
+			const successBox = document.createElement('div');
+			successBox.className = 'success-box';
+
+			if (data.title) {
+				const title = document.createElement('h3');
+				title.textContent = data.title;
+				successBox.appendChild(title);
+			}
+
+			if (data.description) {
+				const descriptions = Array.isArray(data.description)
+					? data.description
+					: [data.description];
+
+				descriptions.forEach(desc => {
+					const p = document.createElement('p');
+					p.textContent = desc;
+					successBox.appendChild(p);
+				});
+			}
+
+			form.insertBefore(successBox, form.firstChild);
+		}
+
+		//  DELETE CACHED FORM DATA ON SUCCESS
+		if (form.dataset.formId) {
+			this.store.delete(form.dataset.formId).catch(err => {
+				console.warn('Failed to clear form cache:', err);
+			});
+
+			// Clear form config dirty state
+			const formConfig = this.forms.get(form.dataset.formId);
+			if (formConfig) {
+				formConfig.isDirty = false;
+				formConfig.lastSaved = Date.now();
+				formConfig.data = {}; // Clear cached data
+			}
+		}
+
+		// Announce success for accessibility
+		if (window.jvbA11y) {
+			window.jvbA11y.announce(data.message || 'Form submitted successfully');
+		}
+
+		// Trigger custom event
+		form.dispatchEvent(new CustomEvent('jvb-form-success', {
+			detail: data
+		}));
+	}
+
+	handleFormError(form, data) {
+		// Clear all previous errors
+		form.querySelectorAll('.error-message').forEach(el => el.remove());
+		form.querySelectorAll('.field-error, .has-error').forEach(el => {
+			el.classList.remove('field-error', 'has-error');
+		});
+
+		// Clear validation states using existing method
+		form.querySelectorAll('.field').forEach(fieldWrapper => {
+			this.clearValidation(fieldWrapper);
+		});
+
+		// Handle field-specific errors
+		if (data.field) {
+			const fieldWrapper = form.querySelector(`[data-field="${data.field}"]`);
+			if (fieldWrapper) {
+				// Use existing showError method for consistency
+				this.showError(fieldWrapper, data.message);
+
+				// Mark as touched so validation persists
+				this.touchedFields.add(data.field);
+
+				// Scroll to error
+				fieldWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
+				// Focus the input for better UX
+				const input = fieldWrapper.querySelector('input, textarea, select');
+				if (input) {
+					input.focus();
+				}
+			}
+		} else {
+			// General form error (not field-specific)
+			const error = document.createElement('div');
+			error.className = 'form-error error-message';
+			error.textContent = data.message;
+
+			// Add icon for consistency
+			const icon = window.getIcon?.('close-circle');
+			if (icon) {
+				icon.classList.add('error-icon');
+				error.prepend(icon);
+			}
+
+			form.insertBefore(error, form.firstChild);
+
+			// Scroll to top to show the error
+			form.scrollIntoView({ behavior: 'smooth', block: 'start' });
+		}
+
+		// Announce error for accessibility
+		if (window.jvbA11y) {
+			const announcement = data.field
+				? `Error in ${data.field}: ${data.message}`
+				: `Form error: ${data.message}`;
+			window.jvbA11y.announce(announcement);
+		}
+
+		// Trigger custom event
+		form.dispatchEvent(new CustomEvent('jvb-form-error', {
+			detail: data
+		}));
+	}
+
 	handleClick(e) {
 		if (window.targetCheck(e, 'div.quantity')) {
 			let container = window.targetCheck(e, 'div.quantity');
 			this.handleNumberClick(e, container.querySelector('input'));
+		} else if (window.targetCheck(e, '[data-action]')) {
+			let actionEl = window.targetCheck(e, '[data-action]');
+			let action = actionEl.dataset.action;
+			let form = actionEl.closest('form');
+
+			switch (action) {
+				case 'clear-form':
+					if (form?.dataset.formId) {
+						this.store.delete(form.dataset.formId);
+						form.reset();
+						// Hide the status message
+						form.querySelector('.fstatus').hidden = true;
+					}
+					if (window.jvbA11y) {
+						window.jvbA11y.announce('Form cleared, starting fresh');
+					}
+					break;
+
+				case 'dismiss-restore':
+					form.querySelector('.fstatus').hidden = true;
+					break;
+			}
 		}
 	}
 
+
 	handleNumberClick(e, input) {
 		let change = 0;
 
@@ -574,15 +1206,16 @@
 	}
 
 	handleChange(event) {
-		if (this.subscribers.size > 0) {
-			const target = event.target;
-			const form = target.form || target.closest('form');
+		if (event.target.closest('[data-ignore]') || this.isRestoring) {
+			return;
+		}
+		const target = event.target;
+		const form = target.form || target.closest('form');if (!form) return;
 
-			if (!form) return;
+		const formConfig = this.forms?.get(form.dataset.formId);
+		if (!formConfig) return;
 
-			const formConfig = this.forms?.get(form.dataset.formId);
-			if (!formConfig) return;
-
+		if (formConfig.options.autosave || this.subscribers.size > 0) {
 			// Check conditional fields
 			const dependencies = formConfig.dependencies.get(target.name);
 			if (dependencies) {
@@ -592,35 +1225,311 @@
 			}
 
 			// Schedule auto-save if enabled
-			if (formConfig.options.autoSave && !form.dataset.noautosave) {
-				const delay = this.getDelayForField(target);
-				this.scheduleSave(formConfig, delay);
-			}
+			const delay = this.getDelayForField(target);
+			this.scheduleSave(formConfig, delay);
 		}
 	}
 
-	handleFocus(event) {
-		const target = event.target;
-		if (target.matches('input, textarea, select')) {
-			// Track focus for better UX
-			this.currentFocus = target;
+	handleBlur(e) {
+		if (e.target.closest('[data-ignore]') || this.isRestoring) {
+			return;
 		}
-	}
-
-	handleBlur(event) {
-		const target = event.target;
+		const target = e.target;
 		const form = target.form || target.closest('form');
 
 		if (!form) return;
 
-		const formConfig = this.forms?.get(form.dataset.formId);
-		if (formConfig && formConfig.options.autoSave && !form.dataset.noautosave) {
-			// Shorter delay on blur
-			this.scheduleSave(formConfig, {
-				type: 'blur',
-				fieldName: target.name,
-				delay: 1500
-			});
+
+		const input = e.target.closest('input, textarea, select');
+		if (input) {
+			const fieldWrapper = this.findFieldWrapper(input);
+			if (fieldWrapper) {
+				// Mark as touched and validate
+				const fieldName = fieldWrapper.dataset.field;
+				if (fieldName) {
+					if (this.shouldDebounce(input)) {
+						window.debouncer.cancel(`validate_${fieldName}`);
+					}
+					this.touchedFields.add(fieldName);
+				}
+				this.validateField(input, fieldWrapper);
+			}
+			const formConfig = this.forms?.get(form.dataset.formId);
+			if (formConfig) {
+				// Shorter delay on blur
+				this.scheduleSave(formConfig, {
+					type: 'blur',
+					fieldName: target.name,
+					delay: 1500
+				});
+			}
+		}
+	}
+
+	handleInput(e) {
+		if (e.target.closest('[data-ignore]') || ! e.target.closest('form') || this.isRestoring) {
+			return;
+		}
+		const input = e.target.closest('input, textarea, select');
+		if (!input) return;
+
+		let form = input.closest('form');
+		this.showFormStatus(form.dataset.formId, 'pending');
+
+		const fieldWrapper = this.findFieldWrapper(input);
+		if (!fieldWrapper) return;
+
+		const fieldName = fieldWrapper.dataset.field;
+		if (fieldName) {
+			this.touchedFields.add(fieldName);
+		}
+
+		if (this.shouldDebounce(input)){
+			window.debouncer.schedule(
+				`validate_${fieldName}`,
+				() => this.validateField.bind(this),
+				500
+			)
+		}
+	}
+
+	/***************************************************************
+	 FORM VALIDATION
+	***************************************************************/
+	/**
+	 * Initialize validation rules
+	 */
+	initValidators() {
+		return {
+			email: {
+				pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
+				message: 'Please enter a valid email address'
+			},
+			url: {
+				pattern: /^https?:\/\/.+\..+/,
+				message: 'Please enter a valid URL starting with https://'
+			},
+			phone: {
+				pattern: /^[\d\s\-+().]+$/,
+				message: 'Please enter a valid phone number'
+			},
+			number: {
+				test: (value, fieldWrapper) => {
+					const num = parseFloat(value);
+					if (isNaN(num)) return 'Please enter a valid number';
+
+					const min = fieldWrapper.dataset.min;
+					const max = fieldWrapper.dataset.max;
+
+					if (min !== undefined && num < parseFloat(min)) {
+						return `Value must be at least ${min}`;
+					}
+					if (max !== undefined && num > parseFloat(max)) {
+						return `Value must be at most ${max}`;
+					}
+					return true;
+				}
+			},
+			text: {
+				test: (value, fieldWrapper) => {
+					const minLength = fieldWrapper.dataset.minlength;
+					const maxLength = fieldWrapper.dataset.maxlength;
+
+					if (minLength && value.length < parseInt(minLength)) {
+						return `Must be at least ${minLength} characters`;
+					}
+					if (maxLength && value.length > parseInt(maxLength)) {
+						return `Must be no more than ${maxLength} characters`;
+					}
+					return true;
+				}
+			}
+		};
+	}
+	/**
+	 * Find the field wrapper (handles both simple and complex fields)
+	 */
+	findFieldWrapper(input) {
+		// Try to find the closest .field wrapper
+		let wrapper = input.closest('.field');
+
+		// If we're in a repeater row, make sure we get the right field wrapper
+		if (!wrapper) {
+			wrapper = input.closest('[data-field]');
+		}
+
+		return wrapper;
+	}
+
+	/**
+	 * Check if input should be debounced
+	 */
+	shouldDebounce(input) {
+		const debounceTypes = ['text', 'email', 'url', 'tel', 'search'];
+		return debounceTypes.includes(input.type) || input.tagName === 'TEXTAREA';
+	}
+
+	/**
+	 * Validate a single field
+	 */
+	validateField(input, fieldWrapper) {
+		const value = this.getFieldValue(input);
+		const fieldName = fieldWrapper.dataset.field;
+
+		// Skip validation if field hasn't been touched yet (unless it's required)
+		if (!this.touchedFields.has(fieldName) && !input.required) {
+			return true;
+		}
+
+		// Skip validation if field is empty and not required
+		if (!value && !input.required) {
+			this.clearValidation(fieldWrapper);
+			return true;
+		}
+
+		// Check required
+		if (input.required && !value) {
+			this.showError(fieldWrapper, 'This field is required');
+			return false;
+		}
+
+		// Check HTML5 validity first
+		if (input.checkValidity && !input.checkValidity()) {
+			this.showError(fieldWrapper, input.validationMessage);
+			return false;
+		}
+
+		// Custom pattern validation from data attribute
+		const pattern = fieldWrapper.dataset.pattern;
+		if (pattern && value) {
+			const regex = new RegExp(pattern);
+			if (!regex.test(value)) {
+				const message = fieldWrapper.dataset.validationMessage || 'Invalid format';
+				this.showError(fieldWrapper, message);
+				return false;
+			}
+		}
+
+		// Type-specific validation
+		const validateType = fieldWrapper.dataset.validate || input.type;
+		if (validateType && this.validators[validateType]) {
+			const validator = this.validators[validateType];
+
+			if (validator.pattern && !validator.pattern.test(value)) {
+				this.showError(fieldWrapper, validator.message);
+				return false;
+			}
+
+			if (validator.test) {
+				const result = validator.test(value, fieldWrapper);
+				if (result !== true) {
+					this.showError(fieldWrapper, result);
+					return false;
+				}
+			}
+		}
+
+		// All validations passed
+		this.showSuccess(fieldWrapper);
+		this.notify('field-validated', input);
+		return true;
+	}
+
+
+
+	/**
+	 * Show success state (green checkmark)
+	 */
+	showSuccess(fieldWrapper, textMessage = '') {
+		if (!fieldWrapper) return;
+
+		// Find validation elements (they might be in field-input-wrapper or field-content)
+		const success = fieldWrapper.querySelector('.validation-icon.success');
+		const error = fieldWrapper.querySelector('.validation-icon.error');
+		const message = fieldWrapper.querySelector('.validation-message');
+		const input = fieldWrapper.querySelector('input, textarea, select');
+
+		// Remove error state
+		fieldWrapper.classList.remove('has-error');
+		input?.classList.remove('error');
+
+		// Add success state
+		fieldWrapper.classList.add('has-success');
+
+		// Show checkmark (if element exists)
+		if (success) {
+			success.hidden = false;
+		}
+		if (error) {
+			error.hidden = true;
+		}
+
+		// Hide error message
+		if (message) {
+			if (textMessage === '') {
+				message.hidden = true;
+				message.textContent = '';
+			} else {
+				message.hidden = false;
+				message.textContent = textMessage;
+			}
+		}
+	}
+
+	/**
+	 * Show error state (red message below field)
+	 */
+	showError(fieldWrapper, errorMessage) {
+		if (!fieldWrapper) return;
+
+		const success = fieldWrapper.querySelector('.validation-icon.success');
+		const error = fieldWrapper.querySelector('.validation-icon.error');
+		const message = fieldWrapper.querySelector('.validation-message');
+		const input = fieldWrapper.querySelector('input, textarea, select');
+
+		// Remove success state
+		fieldWrapper.classList.remove('has-success');
+
+		// Add error state
+		fieldWrapper.classList.add('has-error');
+		input?.classList.add('error');
+
+		// Hide checkmark (if element exists)
+		if (success) {
+			success.hidden = true;
+		}
+		//show x
+		if (error) {
+			error.hidden = false;
+		}
+
+		// Show error message
+		if (message) {
+			message.hidden = false;
+			message.textContent = errorMessage;
+		}
+	}
+
+	/**
+	 * Clear validation state
+	 */
+	clearValidation(fieldWrapper) {
+		if (!fieldWrapper) return;
+
+		const icon = fieldWrapper.querySelector('.validation-icon');
+		const message = fieldWrapper.querySelector('.validation-message');
+		const input = fieldWrapper.querySelector('input, textarea, select');
+
+		fieldWrapper.classList.remove('has-error', 'has-success');
+		input?.classList.remove('error');
+
+		if (icon) {
+			icon.hidden = true;
+		}
+
+		if (message) {
+			message.hidden = true;
+			message.textContent = '';
 		}
 	}
 
@@ -629,7 +1538,6 @@
 	 * Get appropriate delay based on field type and context
 	 */
 	getDelayForField(field) {
-		console.log('Get Delay for Field', field);
 		// Text fields get longer delay for typing
 		if (field.type === 'text' || field.type === 'textarea') {
 			return this.autoSaveDefaults.typingDelay;
@@ -644,7 +1552,10 @@
 		return this.autoSaveDefaults.delay;
 	}
 	scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) {
-		document.addEventListener('input', this.handleInput, {passive: true});
+		if (!formConfig.options.autosave) {
+			return;
+		}
+		document.addEventListener('input', this.saveCheck, {passive: true});
 		const saveKey = `autosave_${formConfig.id}`;
 
 		this.debouncer.schedule(
@@ -655,17 +1566,35 @@
 	}
 
 	//Extend delay if user is currently typing
-	handleInput(e) {
+	saveCheck(e) {
 		let form = e.target.closest('form[data-id]');
 		if (!form) {
 			return;
 		}
+
 		this.scheduleSave(this.forms.get(form.dataset.id));
 	}
 
 	async autosave(formConfig) {
 		const formData = this.collectFormData(formConfig.element);
-		this.cacheFormData(formConfig, formData);
+
+		this.showFormStatus(formConfig.id, 'saving');
+
+		// DataStore will now automatically:
+		// - Convert Sets/Maps to Arrays/Objects
+		// - Strip DOM references
+		// - Validate serializability
+		await this.store.save({
+			formId: formConfig.id,
+			data: formData,
+			status: 'draft',
+			timestamp: Date.now()
+		}).then(() => {
+			this.showFormStatus(formConfig.id, 'autosaved');
+		}).catch(error => {
+			console.error('Autosave failed:', error);
+			this.showFormStatus(formConfig.id, 'error', 'Failed to save changes');
+		});
 
 		// Get only changed fields
 		const changes = this.getChangedFields(formConfig.data, formData);
@@ -676,13 +1605,14 @@
 		this.forms.set(formConfig.id, formConfig);
 		document.removeEventListener('input', this.handleInput);
 
-		for (let [key,  value] of Object.entries(formData)) {
-			//We want all data for complex fields, like group, repeater, or location
+		for (let [key, value] of Object.entries(formData)) {
+			// Complex fields need full data
 			if (typeof value === 'object') {
 				changes[key] = value;
 			}
 		}
-		// Notify instead of callback
+
+		// Notify
 		this.notify('form-autosave', {
 			formId: formConfig.id,
 			changes: changes,
@@ -691,20 +1621,6 @@
 		});
 	}
 
-	cacheFormData(formConfig, formData) {
-		try {
-			this.store.storeForm(formConfig.id, {
-				formId: formConfig.id,
-				formData: formData,
-				timestamp: Date.now(),
-				status: 'pending',
-				operationId: null
-			});
-		} catch (error) {
-			console.error('Failed to cache form data:', error);
-		}
-	}
-
 	/**
 	 * Check if form has unsaved changes
 	 */
@@ -717,35 +1633,78 @@
 
 		// Check if current data differs from snapshot
 		const currentData = this.collectFormData(formConfig.element);
-		const changes = this.getChangedFields(formConfig.lastSnapshot, currentData);
+		const changes = this.getChangedFields(formConfig.data, currentData);
 
 		return Object.keys(changes).length > 0;
 	}
 
-	showFormStatus(form, status) {
-		// Remove existing status
-		const existingStatus = form.querySelector('.form-status');
-		if (existingStatus) {
-			existingStatus.remove();
+	showFormStatus(formID, status, message='') {
+		let form = this.forms.get(formID);
+		if (!form?.options.formStatus) {
+			return;
 		}
 
-		// Add new status
-		const statusElement = document.createElement('div');
-		statusElement.className = `form-status status-${status}`;
+		if (form.status === status){
+			return;
+		}
+
+		form.status = status;
+
+		const statusWrap = form.element.querySelector('.fstatus');
+		statusWrap.hidden = false;
+		const statusElement = statusWrap.querySelector('.message');
+		statusElement.textContent = '';
+		statusWrap.querySelector('.icon')?.remove();
+		statusWrap.querySelector('.actions')?.remove(); // Clear old actions
 
 		const messages = {
 			'saving': 'Saving changes...',
-			'saved': 'Changes saved',
-			'error': 'Failed to save changes',
+			'autosaved': 'Changes saved locally. Submit form to send to server.',
+			'uploading': 'Uploading your form to server',
+			'submitted': 'Successfully sent to server',
+			'pending': 'Unsaved changes',
+			'restored': 'Welcome back! We\'ve restored your previous entry.',
+			'error': 'Failed to save changes. Refresh and try again?',
 			'offline': 'Changes will be saved when online'
 		};
 
-		statusElement.textContent = messages[status] || status;
-		form.insertBefore(statusElement, form.firstChild);
+		const icons = {
+			'autosaved': 'check-circle',
+			'submitted': 'check-circle',
+			'restored': 'history',
+			'error': 'close-circle',
+			'offline': 'cloud-slash',
+			'pending': 'exclamation-mark'
+		}
+
+		let icon = window.getIcon(icons[status]);
+		if (icon) {
+			statusWrap.prepend(icon);
+		}
+
+		if (message === '') {
+			message = messages[status] || status;
+		}
+		statusElement.textContent = message;
+		statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status));
+
+		// Add action buttons for certain statuses
+		if (status === 'restored') {
+			const actions = document.createElement('div');
+			actions.className = 'actions';
+			actions.innerHTML = `
+            <button type="button" class="button button-small" data-action="dismiss-restore">Got it</button>
+            <button type="button" class="button button-small button-link" data-action="clear-form">Start over</button>
+        `;
+			statusWrap.appendChild(actions);
+
+			// Auto-dismiss after 10 seconds
+			setTimeout(() => statusWrap.hidden = true, 10000);
+		}
 
 		// Auto-hide success messages
-		if (status === 'saved') {
-			setTimeout(() => statusElement.remove(), 3000);
+		if (status === 'submitted') {
+			setTimeout(() => statusWrap.hidden = true, 3000);
 		}
 	}
 
@@ -768,6 +1727,14 @@
 	/* ========== Form Data Methods ========== */
 
 	collectFormData(form) {
+		if (Object.hasOwn(form.dataset, 'timeline')) {
+			return this.collectTimeline(form);
+		}
+		//Table forms are handled separately
+		if (form.classList.contains('table') && form.tagName === 'FORM') {
+			return {};
+		}
+
 		const formData = new FormData(form);
 		let data = {};
 		const repeaterData = {};
@@ -779,18 +1746,62 @@
 			const processor = this.getFieldProcessor(key);
 			processor(key, value, data, repeaterData, postData, form);
 		}
-		if (!window.isEmptyObject(postData)) {
+		if (Object.keys(postData).length !== 0) {
 			data = this.mergeRepeaterData(data, repeaterData);
 			return this.mergePostData(data, postData);
 		}
 		return this.mergeRepeaterData(data, repeaterData);
 	}
 
+	collectTimeline(form) {
+		let data = {};
+		let posts = {}; // Temporary object keyed by post ID
+		let postOrder = []; // Track order as encountered (preserves DOM/drag order)
+		let formData = new FormData(form);
+
+		for (const [key, value] of formData.entries()) {
+			if (this.ignore.includes(key) || key.endsWith('_temp')) {
+				continue;
+			}
+			const match = key.match(/^\[(\d+)](.+)$/);
+			if (match) {
+				// Timeline-specific field: [postId]fieldName
+				const [, postId, fieldName] = match;
+				if (!posts[postId]) {
+					posts[postId] = {
+						id: parseInt(postId),
+					};
+					postOrder.push(postId); // Track first occurrence
+				}
+				if (fieldName === 'post_thumbnail') {
+					posts[postId]['post_thumbnail'] = parseInt(form.querySelector(`[name="${key}"]`).closest('.item')?.dataset.id);
+				} else {
+					const processor = this.getFieldProcessor(fieldName);
+					processor(fieldName, value, posts[postId], {}, {}, form);
+				}
+
+			} else {
+				// Shared field (post_title, taxonomies, etc.)
+				const processor = this.getFieldProcessor(key);
+				processor(key, value, data, {}, {}, form);
+			}
+		}
+
+		// Convert to array in DOM order (matches menu_order)
+		data.timeline = postOrder.map(id => posts[id]);
+
+		delete data['form-id'];
+		delete data['sendAll'];
+		delete data['timeline_temp'];
+		delete data['']; // Empty key
+
+		return data;
+	}
+
 	getFieldProcessor(key) {
-		if (key.includes('|')) return this.processTableField;
 		if (key.includes('::')) return this.processGroupField;
 		if (key.includes(':')) return this.processRepeaterField;
-		if (key.includes('[')) return this.processLocationField;
+		if (/\[[^\]]+]/.test(key)) return this.processLocationField;
 		return this.processRegularField;
 	}
 
@@ -812,42 +1823,12 @@
 	}
 
 	mergePostData(data, postData) {
-		for (let [postId, postData] in Object.entries(postData)) {
-			data[postId] = postData;
+		for (let [postId, fields] of Object.entries(postData)) {
+			data[postId] = fields;
 		}
 		return data;
 	}
 
-	processTableField(key, value, data, repeaterData, postData, form) {
-		/***
-		 * Table forms are a huge form containing multiple posts and their data
-		 * Field names are prepended with  `${postID}|`
-		 * Goal:
-		 * 1) Separate out the post id from the field name
-		 * 2) store the original data in a temporary 'original' variable
-		 * 3) Process the field as normal
-		 * 4) return the original data, as PostID: {$field data}
-		 * Final format:
-		 * {
-		 *     id1: {
-		 *         field1: "A title",
-		 *         field3: 32
-		 *     },
-		 *     id2: {
-		 *         field1: "Another title",
-		 *         field2: "122,21,32"
-		 *     }
-		 * }
-		 **/
-		let [post, fieldKey] = key.split('|');
-		if (!post in postData) {
-			postData[post] = {};
-		}
-
-		const processor = this.getFieldProcessor(fieldKey);
-		processor(fieldKey, value, postData, repeaterData, postData, form);
-
-	}
 	processRepeaterField(key, value, data, repeaterData, postData, form) {
 		let [fieldName, index, subField] = key.split(':');
 
@@ -924,6 +1905,7 @@
 
 	processRegularField(key, value, data, repeaterData, postData, form) {
 		//handle array values (like checkboxes/selects)
+		key = key.replace('[]','');
 		if (data[key]) {
 			if (!Array.isArray(data[key])) {
 				data[key] = [data[key]];
@@ -934,25 +1916,487 @@
 		}
 	}
 
-	getFieldValue(field) {
-		if (!field) return '';
+	/**
+	 * Get field value (handles different input types)
+	 */
+	getFieldValue(input) {
+		if (!input) return '';
 
-		if (field.type === 'checkbox') {
-			return field.checked ? field.value || '1' : '';
-		} else if (field.type === 'radio') {
-			const checked = field.form.querySelector(`[name="${field.name}"]:checked`);
+		if (input.type === 'checkbox') {
+			return input.checked ? input.value || '1' : '';
+		} else if (input.type === 'radio') {
+			const checked = input.form?.querySelector(`[name="${input.name}"]:checked`);
 			return checked ? checked.value : '';
-		} else if (field.type === 'select-multiple') {
-			return Array.from(field.selectedOptions).map(o => o.value);
-		} else {
-			return field.value;
+		} else if (input.type === 'select-multiple') {
+			return Array.from(input.selectedOptions).map(o => o.value);
 		}
+
+		return input.value?.trim() || '';
 	}
 
 	getChangedFields(original, current) {
 		return window.getDifferences?.map(original, current) || {};
 	}
 
+	/*******************************************************
+	 Field Summary
+	*******************************************************/
+	/**
+	 * Show a comprehensive summary of form submission
+	 */
+	showSummary(formId, clear = 'form') {
+		const formConfig = this.forms.get(formId);
+		if (!formConfig) return;
+
+		const form = formConfig.element || document.querySelector(`[data-form-id="${formId}"]`);
+		const summary = window.getTemplate('formSummary');
+		if (!summary) return;
+		const wrapper = summary.querySelector('.result');
+
+		// Fields to skip in summary
+		const skipFields = ['sendAll', ...this.ignore];
+
+		// Process each field in the form data
+		for (const [key, value] of Object.entries(formConfig.data)) {
+			// Skip ignored fields and empty values
+			if (skipFields.includes(key) || this.isEmptyValue(value)) {
+				continue;
+			}
+
+			// Get field info from form
+			const fieldInfo = this.getFieldInfo(form, key);
+
+			if (!fieldInfo.label) continue; // Skip if no label found
+
+			let field = wrapper.cloneNode(true);
+			let title = field.querySelector('h3');
+			let p = field.querySelector('p');
+
+			title.textContent = fieldInfo.label;
+
+
+			let formatted = this.formatFieldValue(value, fieldInfo.type, form);
+			if (this.isHtmlContent(formatted)) {
+				p.innerHTML = formatted;
+			} else {
+				p.textContent = formatted;
+			}
+
+			summary.append(field);
+		}
+		let uploads = form.querySelectorAll('[data-upload-field]');
+		if (uploads) {
+			uploads.forEach(upload => {
+				let label = upload.querySelector('h2').textContent;
+
+				let imgs = upload.querySelectorAll('.item-grid.preview img');
+				if (imgs) {
+					let field = wrapper.cloneNode(true);
+					let title = field.querySelector('h3');
+					let p = field.querySelector('p');
+					p.remove();
+
+					title.textContent = label;
+					imgs.forEach(img => {
+						img = img.cloneNode(true);
+						field.append(img);
+					});
+					summary.append(field);
+				}
+			});
+		}
+
+		// Remove template
+		wrapper.remove();
+
+		// Insert summary and hide form
+		clear = (clear !== 'form') ? form.closest(clear)??form : form;
+
+		clear.after(summary);
+		window.fade(clear, false);
+	}
+
+	/**
+	 * Check if a value is empty (null, undefined, empty string, empty array, empty object)
+	 */
+	isEmptyValue(value) {
+		if (value === null || value === undefined || value === '') {
+			return true;
+		}
+		if (Array.isArray(value) && value.length === 0) {
+			return true;
+		}
+		return typeof value === 'object' && Object.keys(value).length === 0;
+
+	}
+
+	/**
+	 * Get field information (label, type, etc.) from the form
+	 * Handles special field name patterns ([], ::, :, etc.)
+	 */
+	getFieldInfo(form, fieldName) {
+		// Try to find label by 'for' attribute (exact match)
+		let label = form.querySelector(`label[for="${fieldName}"]`);
+		let input = form.querySelector(`[name=${fieldName}]`);
+		// Try to find the input field - check multiple patterns
+		if (!input) {
+			// Try exact match first
+			input = form.querySelector(`[name="${fieldName}"]`);
+		}
+
+		if (!input) {
+			// Try with [] suffix (for checkboxes, multi-selects)
+			input = form.querySelector(`[name="${fieldName}[]"]`);
+		}
+
+		if (!input) {
+			// Try as fieldset legend (for checkbox/radio groups)
+			const fieldset = form.querySelector(`fieldset[data-field="${fieldName}"]`);
+			if (fieldset) {
+				label = fieldset.querySelector('legend');
+				input = fieldset.querySelector('input, select, textarea');
+			}
+		}
+
+		// Get label from input if not found yet
+		if (!label && input) {
+			// Try closest field wrapper first
+			const field = input.closest('.field, fieldset');
+			if (field) {
+				label = field.querySelector('label, legend, h2');
+			}
+		}
+
+		// Get field wrapper - always use base name (no special characters)
+		let fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"], fieldset[data-field="${fieldName}"]`);
+
+		// Determine field type
+		let fieldType = 'text';
+		if (fieldWrapper?.dataset.type) {
+			fieldType = fieldWrapper.dataset.type;
+		} else if (input) {
+			// Infer from input type
+			if (input.type === 'checkbox' && input.name.endsWith('[]')) {
+				fieldType = 'checkbox'; // checkbox group
+			} else if (input.type === 'checkbox') {
+				fieldType = 'true_false'; // single checkbox
+			} else if (input.tagName === 'SELECT' && input.multiple) {
+				fieldType = 'select'; // multi-select
+			} else {
+				fieldType = input.type || 'text';
+			}
+		}
+
+		return {
+			label: label?.textContent.replace('*', '').trim() || null,
+			type: fieldType,
+			wrapper: fieldWrapper,
+			input: input
+		};
+	}
+
+	/**
+	 * Check if content should be treated as HTML
+	 */
+	isHtmlContent(content) {
+		return typeof content === 'string' && (
+			content.includes('<br>') ||
+			content.includes('<p>') ||
+			content.includes('<ul>') ||
+			content.includes('<ol>') ||
+			content.includes('<a ') ||
+			content.includes('<strong>') ||
+			content.includes('<em>') ||
+			content.includes('<div')
+		);
+	}
+
+	/**
+	 * Format field value based on type
+	 */
+	formatFieldValue(value, type, form) {
+		switch (type) {
+			case 'textarea':
+			case 'wysiwyg':
+				// Handle rich text - check if it's actual HTML content from Quill
+				return this.formatTextareaValue(value, type);
+
+			case 'true_false':
+				return (value === '1' || value === 1 || value === true) ? 'Yes' : 'No';
+			case 'checkbox':
+				// Handle both single checkbox and checkbox groups
+				if (Array.isArray(value)) {
+					return this.formatArrayValue(value);
+				}
+				return (value === '1' || value === 1 || value === true) ? 'Yes' : value;
+
+			case 'select':
+				// Handle both single and multi-select
+				if (Array.isArray(value)) {
+					return this.formatArrayValue(value);
+				}
+				// Get label from select option
+				return this.getSelectLabel(value, form, type);
+			case 'date':
+			case 'datetime':
+			case 'time':
+				return window.formatDate ? window.formatDate(value) : value;
+
+			case 'radio':
+				// Get label from select option or radio label
+				return this.getSelectLabel(value, form, type);
+
+			case 'repeater':
+				return this.formatRepeaterValue(value);
+
+			case 'group':
+				return this.formatGroupValue(value);
+
+			case 'location':
+				return this.formatLocationValue(value);
+
+			case 'upload':
+				return this.formatFileValue(value);
+
+			case 'number':
+				return this.formatNumber(value);
+
+			case 'email':
+				return `<a href="mailto:${value}">${value}</a>`;
+
+			case 'url':
+				return `<a href="${value}" target="_blank" rel="noopener">${value}</a>`;
+
+			case 'phone':
+				return `<a href="tel:${value.replace(/\D/g, '')}">${value}</a>`;
+
+			default:
+				// Handle arrays (multi-select, checkbox group)
+				if (Array.isArray(value)) {
+					return this.formatArrayValue(value);
+				}
+				return value;
+		}
+	}
+
+	/**
+	 * Format repeater field value
+	 */
+	formatRepeaterValue(rows) {
+		if (!Array.isArray(rows) || rows.length === 0) {
+			return '<em>No entries</em>';
+		}
+
+		let html = '<div class="repeater-summary">';
+		rows.forEach((row, index) => {
+			html += `<div class="repeater-row">`;
+			html += `<strong>Entry ${index + 1}:</strong><ul>`;
+			for (const [key, value] of Object.entries(row)) {
+				if (!this.isEmptyValue(value)) {
+					const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+					html += `<li><strong>${label}:</strong> ${value}</li>`;
+				}
+			}
+			html += `</ul></div>`;
+		});
+		html += '</div>';
+		return html;
+	}
+
+	/**
+	 * Format group field value
+	 */
+	formatGroupValue(groupData) {
+		if (typeof groupData !== 'object' || Object.keys(groupData).length === 0) {
+			return '<em>No data</em>';
+		}
+
+		let html = '<div class="group-summary"><ul>';
+		for (const [key, value] of Object.entries(groupData)) {
+			if (!this.isEmptyValue(value)) {
+				const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+				// Handle nested groups
+				if (typeof value === 'object' && !Array.isArray(value)) {
+					html += `<li><strong>${label}:</strong> ${this.formatGroupValue(value)}</li>`;
+				} else {
+					html += `<li><strong>${label}:</strong> ${value}</li>`;
+				}
+			}
+		}
+		html += '</ul></div>';
+		return html;
+	}
+
+	/**
+	 * Format location field value
+	 */
+	formatLocationValue(location) {
+		if (typeof location !== 'object') return location;
+
+		const parts = [];
+		const fields = ['address', 'city', 'state', 'zip', 'country'];
+
+		fields.forEach(field => {
+			if (location[field]) {
+				parts.push(location[field]);
+			}
+		});
+
+		return parts.join(', ');
+	}
+
+	/**
+	 * Format file/image value
+	 */
+	formatFileValue(value) {
+		if (typeof value === 'string') {
+			// Single file - could be URL or filename
+			if (value.startsWith('http')) {
+				return `<a href="${value}" target="_blank">View file</a>`;
+			}
+			return value;
+		}
+
+		if (Array.isArray(value)) {
+			return value.map(file => {
+				if (typeof file === 'string') {
+					return `<a href="${file}" target="_blank">View file</a>`;
+				}
+				return file.name || 'File';
+			}).join(', ');
+		}
+
+		return 'File uploaded';
+	}
+
+	/**
+	 * Format number with proper locale formatting
+	 */
+	formatNumber(value) {
+		const num = parseFloat(value);
+		if (isNaN(num)) return value;
+
+		// Check if it's likely currency (has 2 decimal places)
+		if (value.toString().includes('.') && value.toString().split('.')[1].length === 2) {
+			return new Intl.NumberFormat('en-CA', {
+				style: 'currency',
+				currency: 'USD'
+			}).format(num);
+		}
+
+		return new Intl.NumberFormat('en-CA').format(num);
+	}
+
+	/**
+	 * Format array values (checkboxes, multi-select)
+	 */
+	/**
+	 * Format array values (checkboxes, multi-select)
+	 */
+	formatArrayValue(arr, form = null, fieldInfo = null) {
+		if (arr.length === 0) return '<em>None selected</em>';
+
+		// If we have field info, try to get proper labels
+		if (form && fieldInfo && fieldInfo.input) {
+			const labeled = arr.map(val => {
+				return this.getSelectLabel(val, form, fieldInfo.type);
+			});
+			return '<ul><li>' + labeled.join('</li><li>') + '</li></ul>';
+		}
+
+		// Fallback to raw values
+		return '<ul><li>' + arr.join('</li><li>') + '</li></ul>';
+	}
+
+	/**
+	 * Get label for select/radio option
+	 */
+	/**
+	 * Get label for select/radio/checkbox option
+	 */
+	getSelectLabel(value, form, type) {
+		if (type === 'select') {
+			const option = form.querySelector(`option[value="${value}"]`);
+			return option?.textContent || value;
+		}
+
+		if (type === 'radio') {
+			const radio = form.querySelector(`input[type="radio"][value="${value}"]`);
+			const label = radio?.nextElementSibling;
+			return label?.textContent || value;
+		}
+
+		if (type === 'checkbox') {
+			// Try to find the checkbox with this value
+			const checkbox = form.querySelector(`input[type="checkbox"][value="${value}"]`);
+			if (checkbox) {
+				// Look for associated label
+				const label = form.querySelector(`label[for="${checkbox.id}"]`);
+				if (label) {
+					return label.textContent.trim();
+				}
+				// Try next sibling
+				const nextLabel = checkbox.nextElementSibling;
+				if (nextLabel?.tagName === 'LABEL') {
+					return nextLabel.textContent.trim();
+				}
+			}
+		}
+
+		return value;
+	}
+
+	/**
+	 * Format textarea value - handles both rich text and plain text
+	 */
+	formatTextareaValue(value, type) {
+		if (!value) return '<em>Empty</em>';
+
+		// If it's explicitly a wysiwyg type or contains HTML tags, use as-is
+		if (type === 'wysiwyg' || this.containsHtml(value)) {
+			// Quill content already has proper HTML structure
+			return value;
+		}
+
+		// Plain textarea - preserve formatting
+		return this.formatPlainText(value);
+	}
+
+	/**
+	 * Check if string contains HTML content (more reliable than just checking for '<')
+	 */
+	containsHtml(str) {
+		// Check for common HTML tags that Quill uses
+		const htmlPattern = /<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i;
+		return htmlPattern.test(str);
+	}
+
+	/**
+	 * Format plain text content - preserves whitespace and converts newlines
+	 */
+	formatPlainText(text) {
+		if (!text) return '';
+
+		// First, escape any HTML entities that might be in the text
+		text = text
+			.replace(/&/g, '&amp;')
+			.replace(/</g, '&lt;')
+			.replace(/>/g, '&gt;');
+
+		// Convert double newlines to paragraphs for better readability
+		const paragraphs = text.split(/\n\n+/);
+
+		if (paragraphs.length > 1) {
+			// Multiple paragraphs
+			return paragraphs
+				.map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`)
+				.join('');
+		}
+
+		// Single paragraph - just convert newlines to breaks
+		return text.replace(/\n/g, '<br>');
+	}
+
 	/**
 	 * Event system
 	 */
@@ -971,7 +2415,6 @@
 	cleanupForm(formId) {
 		const formConfig = this.forms.get(formId);
 		if (!formConfig) return;
-		console.log('Cleaning up form', formConfig);
 
 		// Check for unsaved changes
 		if (this.hasUnsavedChanges(formId)) {
@@ -991,11 +2434,16 @@
 	destroy() {
 		// Remove global handlers
 		if (this.globalHandlersAdded) {
-			document.removeEventListener('submit', this.submitHandler);
 			document.removeEventListener('change', this.changeHandler);
-			document.removeEventListener('focus', this.focusHandler, true);
 			document.removeEventListener('blur', this.blurHandler, true);
+			document.removeEventListener('input', this.inputHandler, true);
 		}
+		this.forms.forEach((formConfig) => {
+			let element = formConfig.element;
+			if (element) {
+				element.removeEventListener('submit', this.submitHandler);
+			}
+		});
 
 		// Clear maps
 		this.specialFields.clear();
@@ -1008,6 +2456,11 @@
 	}
 }
 
-document.addEventListener('DOMContentLoaded', () => {
-	window.jvbForm = FormController;
+document.addEventListener('DOMContentLoaded', async function () {
+	window.auth.subscribe(event => {
+		if (event === 'auth-loaded') {
+			window.jvbForm = FormController;
+		}
+	});
+
 });

--
Gitblit v1.10.0