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

---
 assets/js/concise/FormController.js |  719 +++++++++++++++++++++++++++++++++--------------------------
 1 files changed, 406 insertions(+), 313 deletions(-)

diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 0e53476..a0388e2 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -1,13 +1,10 @@
-/**
- * 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(config = {}) {
 		this.config = {
 			collectFormData: false,
 			... config
 		}
+		this.isRestoring = false;
 		const store = window.jvbStore.register(
 			'forms',
 			{
@@ -57,17 +54,14 @@
 			remove: 800,
 			reorder: 1000
 		};
-		this.isTimeline = false;
-		if (window.crudManager && window.crudManager.isTimeline) {
-			this.isTimeline = true;
-		}
+
+		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);
@@ -127,35 +121,68 @@
 		}
 	}
 
-	checkPendingForms() {
-		// No async needed - data is already loaded in memory
-		const allForms = this.store.getAll();
-		const pendingForms = allForms.filter(form => form.status === 'draft');
+	/**
+	 * 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 form = this.forms.get(item.formId);
-			if (form?.element) {
-				const restoreBtn = form.element.querySelector('.restore-form');
-				if (restoreBtn) {
-					restoreBtn.hidden = false;
-				}
-				new this.populateForm(form.element, item.data);
+			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');
 			}
 		});
 	}
+
 	/**
-	 * Check for pending operations from previous session
+	 * Find form element that matches the cached data
 	 */
-	async checkPendingOperations() {
-		const pendingForms = await this.store.query('status', 'pending');
+	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;
+		}
 
-		if (pendingForms.length === 0) return;
+		// 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;
+		}
 
-		// Group by form type or page
-		const grouped = this.groupPendingForms(pendingForms);
-
-		// Show consolidated notification
-		this.showPendingNotification(grouped);
+		// Fallback: try by formId (if it was already registered)
+		return document.querySelector(`[data-form-id="${formData.formId}"]`);
 	}
 
 	/**
@@ -236,7 +263,6 @@
 		if (!this.globalHandlersAdded) {
 			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;
@@ -259,8 +285,9 @@
 			status: '',
 			options: {
 				autosave: 'autosave' in formElement.dataset,
+				autoUpload: true,
 				saveDelay: this.autoSaveDefaults.delay,
-				endpoint: formElement.dataset.save??'',
+				endpoint: formElement.dataset.save ?? '',
 				formStatus: true,
 				cache: true,
 				...options
@@ -269,17 +296,14 @@
 			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.get(formId);
-			if (cached && cached.formData) {
-				this.showPendingNotification(cached);
+			if (cached && cached.data) {
+				this.showPendingNotification(formId, cached.data);
 			}
 		}
 
@@ -296,6 +320,8 @@
 		// Initialize repeater fields
 		this.initRepeaterFields(form, formConfig);
 
+		this.initTagListFields(form, formConfig);
+
 		// Initialize conditional fields
 		if (formConfig) {
 			this.initConditionalFields(form, formConfig);
@@ -305,7 +331,7 @@
 		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')) {
@@ -586,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) {
@@ -630,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);
@@ -639,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;
 		}
 	}
 
@@ -704,16 +955,16 @@
 	/**
 	 * Initialize image upload fields
 	 */
-	initImageUploadFields(form) {
-		window.jvbUploads.scanFields(form);
+	initImageUploadFields(form, config) {
+		window.jvbUploads.scanFields(form, config.options.autoUpload);
 	}
 
 	/* ========== Event Handlers ========== */
 
 	async handleSubmit(event) {
 		const form = event.target;
-		if (!form.dataset.formId) return;
 
+		if (!form.dataset.formId) return;
 		const formConfig = this.forms.get(form.dataset.formId);
 
 		// Handle subscriber-based forms
@@ -727,14 +978,7 @@
 				fullData: formData,
 				config: formConfig
 			});
-
-			// Don't delete yet - wait for success/error from subscriber
-			return;
 		}
-
-		// For forms that submit normally (not prevented)
-		// We can clean up the cache on successful submission
-		// This would typically be called from handleFormSuccess
 	}
 
 	handleFormSuccess(form, data) {
@@ -787,7 +1031,7 @@
 			form.insertBefore(successBox, form.firstChild);
 		}
 
-		// ✅ DELETE CACHED FORM DATA ON SUCCESS
+		//  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);
@@ -882,17 +1126,25 @@
 			let container = window.targetCheck(e, 'div.quantity');
 			this.handleNumberClick(e, container.querySelector('input'));
 		} else if (window.targetCheck(e, '[data-action]')) {
-			let action = window.targetCheck(e, '[data-action]');
-			action = action.dataset.action;
+			let actionEl = window.targetCheck(e, '[data-action]');
+			let action = actionEl.dataset.action;
+			let form = actionEl.closest('form');
+
 			switch (action) {
 				case 'clear-form':
-					let form = e.target.closest('form');
-					this.store.delete(form.dataset.formId);
-					form?.reset();
-					e.target.closest('.restore-form').hidden = true;
+					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':
-					e.target.closest('.restore-form').hidden = true;
+					form.querySelector('.fstatus').hidden = true;
 					break;
 			}
 		}
@@ -954,7 +1206,7 @@
 	}
 
 	handleChange(event) {
-		if (event.target.closest('[data-ignore]')) {
+		if (event.target.closest('[data-ignore]') || this.isRestoring) {
 			return;
 		}
 		const target = event.target;
@@ -978,16 +1230,8 @@
 		}
 	}
 
-	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]')) {
+		if (e.target.closest('[data-ignore]') || this.isRestoring) {
 			return;
 		}
 		const target = e.target;
@@ -1023,7 +1267,7 @@
 	}
 
 	handleInput(e) {
-		if (e.target.closest('[data-ignore]') || ! e.target.closest('form')) {
+		if (e.target.closest('[data-ignore]') || ! e.target.closest('form') || this.isRestoring) {
 			return;
 		}
 		const input = e.target.closest('input, textarea, select');
@@ -1043,7 +1287,7 @@
 		if (this.shouldDebounce(input)){
 			window.debouncer.schedule(
 				`validate_${fieldName}`,
-				(input, fieldWrapper) => this.validateField.bind(this),
+				() => this.validateField.bind(this),
 				500
 			)
 		}
@@ -1063,10 +1307,10 @@
 			},
 			url: {
 				pattern: /^https?:\/\/.+\..+/,
-				message: 'Please enter a valid URL starting with http:// or https://'
+				message: 'Please enter a valid URL starting with https://'
 			},
 			phone: {
-				pattern: /^[\d\s\-\+\(\)\.]+$/,
+				pattern: /^[\d\s\-+().]+$/,
 				message: 'Please enter a valid phone number'
 			},
 			number: {
@@ -1191,23 +1435,7 @@
 		return true;
 	}
 
-	/**
-	 * Get field value (handles different input types)
-	 */
-	getFieldValue(input) {
-		if (!input) return '';
 
-		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 (input.type === 'select-multiple') {
-			return Array.from(input.selectedOptions).map(o => o.value);
-		}
-
-		return input.value?.trim() || '';
-	}
 
 	/**
 	 * Show success state (green checkmark)
@@ -1305,145 +1533,6 @@
 		}
 	}
 
-	/**
-	 * Validate all fields in a container (useful for step validation)
-	 */
-	validateAllFields(container) {
-		if (!container) return true;
-
-		const fields = container.querySelectorAll('.field:not([hidden])');
-		let allValid = true;
-
-		fields.forEach(fieldWrapper => {
-			// Skip complex parent wrappers (repeater, group) - validate their children
-			if (this.isComplexFieldWrapper(fieldWrapper)) {
-				return;
-			}
-
-			const input = fieldWrapper.querySelector('input:not([type="hidden"]), textarea, select');
-			if (input && !input.closest('[hidden]')) {
-				// Mark as touched so validation will run
-				const fieldName = fieldWrapper.dataset.field;
-				if (fieldName) {
-					this.touchedFields.add(fieldName);
-				}
-
-				const isValid = this.validateField(input, fieldWrapper);
-				if (!isValid) {
-					allValid = false;
-
-					// Scroll to first error
-					if (allValid === false) {
-						input.scrollIntoView({ behavior: 'smooth', block: 'center' });
-						input.focus();
-					}
-				}
-			}
-		});
-
-		return allValid;
-	}
-
-	/**
-	 * Check if field wrapper is a complex type (repeater, group, etc.)
-	 */
-	isComplexFieldWrapper(fieldWrapper) {
-		return fieldWrapper.classList.contains('repeater') ||
-			fieldWrapper.classList.contains('group') ||
-			fieldWrapper.classList.contains('upload');
-	}
-
-	/**
-	 * Special validation for repeater fields
-	 */
-	attachRepeaterValidation(form) {
-		// When a repeater row is added, attach validation to its fields
-		form.addEventListener('click', (e) => {
-			if (e.target.closest('.add-repeater-row')) {
-				// Wait for the DOM to update
-				setTimeout(() => {
-					const repeaterRows = form.querySelectorAll('.repeater-row');
-					repeaterRows.forEach(row => {
-						const inputs = row.querySelectorAll('input, textarea, select');
-						inputs.forEach(input => {
-							const fieldWrapper = this.findFieldWrapper(input);
-							if (fieldWrapper) {
-								// Validation listeners are already attached via event delegation
-								// Just clear any existing validation state for new rows
-								this.clearValidation(fieldWrapper);
-							}
-						});
-					});
-				}, 100);
-			}
-		});
-	}
-
-	/**
-	 * Special validation for group fields
-	 */
-	attachGroupValidation(form) {
-		// Group fields might have conditional fields
-		// Validate when conditions change
-		form.addEventListener('change', (e) => {
-			const changedInput = e.target.closest('input, select');
-			if (!changedInput) return;
-
-			// Check if this change affects conditional fields
-			const fieldName = changedInput.name;
-			if (!fieldName) return;
-
-			// Find any conditional fields that depend on this field
-			const conditionalFields = form.querySelectorAll(`[data-show-if*="${fieldName}"]`);
-			conditionalFields.forEach(conditionalField => {
-				// Clear validation for hidden fields
-				if (conditionalField.hidden) {
-					this.clearValidation(conditionalField);
-				}
-			});
-		});
-	}
-
-	/**
-	 * Reset validation state for a form
-	 */
-	resetForm(form) {
-		if (!form) return;
-
-		// Clear all touched fields
-		this.touchedFields.clear();
-
-		// Clear all validation states
-		const fields = form.querySelectorAll('.field');
-		fields.forEach(fieldWrapper => {
-			this.clearValidation(fieldWrapper);
-		});
-	}
-
-	/**
-	 * Get validation errors for a form
-	 */
-	getFormErrors(form) {
-		const errors = {};
-		const fields = form.querySelectorAll('.field.has-error');
-
-		fields.forEach(fieldWrapper => {
-			const fieldName = fieldWrapper.dataset.field;
-			const message = fieldWrapper.querySelector('.validation-message');
-			if (fieldName && message) {
-				errors[fieldName] = message.textContent;
-			}
-		});
-
-		return errors;
-	}
-
-	/**
-	 * Add custom validator
-	 */
-	addValidator(name, validator) {
-		this.validators[name] = validator;
-	}
 	/* ========== Auto-save functionality ========== */
 	/**
 	 * Get appropriate delay based on field type and context
@@ -1550,9 +1639,8 @@
 	}
 
 	showFormStatus(formID, status, message='') {
-		// Remove existing status
 		let form = this.forms.get(formID);
-		if (!form.options.formStatus) {
+		if (!form?.options.formStatus) {
 			return;
 		}
 
@@ -1562,12 +1650,12 @@
 
 		form.status = status;
 
-		// Add new 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...',
@@ -1575,12 +1663,15 @@
 			'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'
 		};
+
 		const icons = {
 			'autosaved': 'check-circle',
 			'submitted': 'check-circle',
+			'restored': 'history',
 			'error': 'close-circle',
 			'offline': 'cloud-slash',
 			'pending': 'exclamation-mark'
@@ -1590,12 +1681,27 @@
 		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 === 'submitted') {
 			setTimeout(() => statusWrap.hidden = true, 3000);
@@ -1620,7 +1726,7 @@
 
 	/* ========== Form Data Methods ========== */
 
-	collectFormData(form, isInit = false) {
+	collectFormData(form) {
 		if (Object.hasOwn(form.dataset, 'timeline')) {
 			return this.collectTimeline(form);
 		}
@@ -1640,7 +1746,7 @@
 			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);
 		}
@@ -1657,7 +1763,7 @@
 			if (this.ignore.includes(key) || key.endsWith('_temp')) {
 				continue;
 			}
-			const match = key.match(/^\[(\d+)\](.+)$/);
+			const match = key.match(/^\[(\d+)](.+)$/);
 			if (match) {
 				// Timeline-specific field: [postId]fieldName
 				const [, postId, fieldName] = match;
@@ -1695,7 +1801,7 @@
 	getFieldProcessor(key) {
 		if (key.includes('::')) return this.processGroupField;
 		if (key.includes(':')) return this.processRepeaterField;
-		if (/\[[^\]]+\]/.test(key)) return this.processLocationField;
+		if (/\[[^\]]+]/.test(key)) return this.processLocationField;
 		return this.processRegularField;
 	}
 
@@ -1810,19 +1916,22 @@
 		}
 	}
 
-	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) {
@@ -1841,16 +1950,8 @@
 
 		const form = formConfig.element || document.querySelector(`[data-form-id="${formId}"]`);
 		const summary = window.getTemplate('formSummary');
-
-		const [
-			title,
-			resultWrapper,
-			resultTemplate
-		] = [
-			summary.querySelector('h2'),
-			summary.querySelector('.summary'),
-			summary.querySelector('.result')
-		];
+		if (!summary) return;
+		const wrapper = summary.querySelector('.result');
 
 		// Fields to skip in summary
 		const skipFields = ['sendAll', ...this.ignore];
@@ -1864,23 +1965,49 @@
 
 			// Get field info from form
 			const fieldInfo = this.getFieldInfo(form, key);
+
 			if (!fieldInfo.label) continue; // Skip if no label found
 
-			// Create result element
-			const resultEl = this.createResultElement(
-				resultTemplate,
-				fieldInfo,
-				value,
-				form
-			);
+			let field = wrapper.cloneNode(true);
+			let title = field.querySelector('h3');
+			let p = field.querySelector('p');
 
-			if (resultEl) {
-				resultWrapper.appendChild(resultEl);
+			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
-		resultTemplate.remove();
+		wrapper.remove();
 
 		// Insert summary and hide form
 		clear = (clear !== 'form') ? form.closest(clear)??form : form;
@@ -1899,10 +2026,8 @@
 		if (Array.isArray(value) && value.length === 0) {
 			return true;
 		}
-		if (typeof value === 'object' && Object.keys(value).length === 0) {
-			return true;
-		}
-		return false;
+		return typeof value === 'object' && Object.keys(value).length === 0;
+
 	}
 
 	/**
@@ -1912,9 +2037,7 @@
 	getFieldInfo(form, fieldName) {
 		// Try to find label by 'for' attribute (exact match)
 		let label = form.querySelector(`label[for="${fieldName}"]`);
-		let input = null;
-		let fieldWrapper = null;
-
+		let input = form.querySelector(`[name=${fieldName}]`);
 		// Try to find the input field - check multiple patterns
 		if (!input) {
 			// Try exact match first
@@ -1940,12 +2063,12 @@
 			// Try closest field wrapper first
 			const field = input.closest('.field, fieldset');
 			if (field) {
-				label = field.querySelector('label, legend');
+				label = field.querySelector('label, legend, h2');
 			}
 		}
 
 		// Get field wrapper - always use base name (no special characters)
-		fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"], fieldset[data-field="${fieldName}"]`);
+		let fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"], fieldset[data-field="${fieldName}"]`);
 
 		// Determine field type
 		let fieldType = 'text';
@@ -1973,32 +2096,6 @@
 	}
 
 	/**
-	 * Create a result element for a field
-	 */
-	createResultElement(template, fieldInfo, value, form) {
-		const resultEl = template.cloneNode(true);
-		const titleEl = resultEl.querySelector('h4');
-		const valueEl = resultEl.querySelector('p');
-
-		// Set label
-		titleEl.textContent = fieldInfo.label;
-
-		// Format value based on field type
-		const formattedValue = this.formatFieldValue(value, fieldInfo.type, form);
-
-		// Determine how to set the value
-		if (this.isHtmlContent(formattedValue)) {
-			// HTML content - use innerHTML
-			valueEl.innerHTML = formattedValue;
-		} else {
-			// Plain text - use textContent for safety
-			valueEl.textContent = formattedValue;
-		}
-
-		return resultEl;
-	}
-
-	/**
 	 * Check if content should be treated as HTML
 	 */
 	isHtmlContent(content) {
@@ -2031,7 +2128,7 @@
 				if (Array.isArray(value)) {
 					return this.formatArrayValue(value);
 				}
-				return (value === '1' || value === 1 || value === true) ? 'Yes' : 'No';
+				return (value === '1' || value === 1 || value === true) ? 'Yes' : value;
 
 			case 'select':
 				// Handle both single and multi-select
@@ -2058,8 +2155,7 @@
 			case 'location':
 				return this.formatLocationValue(value);
 
-			case 'file':
-			case 'image':
+			case 'upload':
 				return this.formatFileValue(value);
 
 			case 'number':
@@ -2302,13 +2398,6 @@
 	}
 
 	/**
-	 * Convert newlines to <br> tags (kept for backwards compatibility)
-	 */
-	nl2br(text) {
-		return this.formatPlainText(text);
-	}
-
-	/**
 	 * Event system
 	 */
 	subscribe(callback) {
@@ -2346,7 +2435,6 @@
 		// Remove global handlers
 		if (this.globalHandlersAdded) {
 			document.removeEventListener('change', this.changeHandler);
-			document.removeEventListener('focus', this.focusHandler, true);
 			document.removeEventListener('blur', this.blurHandler, true);
 			document.removeEventListener('input', this.inputHandler, true);
 		}
@@ -2368,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