From d7e7d248cbe41cd7a9ef9c2fb022b6c4831f99a3 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 31 May 2026 15:22:56 +0000
Subject: [PATCH] =jakevan complete

---
 assets/js/concise/FormController.js | 2115 ++++++++++++++++++++++++++++++++++++----------------------
 1 files changed, 1,321 insertions(+), 794 deletions(-)

diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 894e58b..3ba05b4 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -19,16 +19,32 @@
 
 		this.isRestoring = false;
 		this.hasListeners = false;
+		this.hasUploads = false;
 		this.summaryTemplate = false;
 
 		this.init();
 	}
 	init() {
 		this.templates = window.jvbTemplates;
+		this.defineSummaryTemplate();
 		this.initElements();
 		this.initListeners();
 		this.initStore();
 		this.initValidators();
+		this.initUploadSubscription();
+	}
+
+	initUploadSubscription() {
+		window.jvbUploads.subscribe((event, data) => {
+			if (!this.hasUploads) return;
+			if (event === 'upload-received') {
+				let form = this.getForm(data.field);
+				if (form) {
+					this.updateItem(`${data.field.dataset.field}_tempUpload`, data.id, form);
+				}
+
+			}
+		});
 	}
 	initElements() {
 		this.inputSelectors = 'input, textarea, select';
@@ -50,7 +66,12 @@
 					status: '.fstatus',
 					message: '.fstatus .message',
 					icon: '.fstatus .icon',
-					actions: '.fstatus .actions'
+					actions: '.fstatus .actions',
+				},
+				restore: {
+					container: '.restore-form',
+					restore: '[data-action="restore"]',
+					clear: '[data-action="clear"]',
 				}
 			},
 			inputs: this.inputSelectors,					//querySelectorAll
@@ -58,7 +79,7 @@
 				field: '.field',					//querySelectorAll
 				label: 'label',
 				success: '.success',
-				error: '.success',
+				error: '.error',
 				message: '.validation-message',
 			},
 			repeater: {
@@ -72,11 +93,12 @@
 			},
 			tagList: {
 				tagList: '.field.tag-list',			//querySelectorAll
-				input: '.tag-input-row',
+				input: '.row',
 				add: '.add-tag',
 				remove: '.remove-tag',
 				label: '.tag-label',
 				items: '.tag-items',
+				item: '.tag-item',
 				inputs: this.inputSelectors,				//querySelectorAll
 				value: 'input[type="hidden"]'		//querySelectorAll
 			},
@@ -90,7 +112,7 @@
 				input: 'input[type="number"]'
 			},
 			limits: {
-				hasLimit: '[data-limit]',
+				hasLimit: '[data-maxlength]',
 				limit: '.limit',
 				current: '.current',
 			}
@@ -107,20 +129,20 @@
 		this.tagListClick = this.handleTagListClick.bind(this);
 		this.tagListInput = this.handleTagListInput.bind(this);
 	}
-		addFormListeners(form) {
-			form.addEventListener('click', this.clickHandler);
-			form.addEventListener('change', this.changeHandler);
-			form.addEventListener('input', this.inputHandler);
-			form.addEventListener('blur', this.blurHandler);
-			form.addEventListener('submit', this.submitHandler);
-		}
-		removeFormListeners(form) {
-			form.removeEventListener('click', this.clickHandler);
-			form.removeEventListener('change', this.changeHandler);
-			form.removeEventListener('input', this.inputHandler);
-			form.removeEventListener('blur', this.blurHandler);
-			form.removeEventListener('submit', this.submitHandler);
-		}
+	addFormListeners(form) {
+		form.addEventListener('click', this.clickHandler);
+		form.addEventListener('change', this.changeHandler);
+		form.addEventListener('input', this.inputHandler);
+		form.addEventListener('blur', this.blurHandler);
+		form.addEventListener('submit', this.submitHandler);
+	}
+	removeFormListeners(form) {
+		form.removeEventListener('click', this.clickHandler);
+		form.removeEventListener('change', this.changeHandler);
+		form.removeEventListener('input', this.inputHandler);
+		form.removeEventListener('blur', this.blurHandler);
+		form.removeEventListener('submit', this.submitHandler);
+	}
 	initStore() {
 		const store = window.jvbStore.register(
 			'forms',
@@ -137,7 +159,7 @@
 		this.store = store.forms;
 
 		this.store.subscribe((event, data)=> {
-			if (event === 'data-loaded') {
+			if (event === 'data-ready') {
 				let stored = this.store.getFiltered();
 
 				let pending = stored.filter(form=> form.src ===  window.location.pathname);
@@ -146,45 +168,64 @@
 				}
 			} else if (event === 'operation-status' && data.status === 'completed') {
 				if (data.config) {
-					this.store.remove(data.config.id);
+					this.store.delete(data.config.id);
 				}
 			}
 		});
 	}
-		showPendingNotification(formId, changes) {
-			let form = this.forms.get(formId);
+	showPendingNotification(formId, changes) {
+		let form = this.forms.get(formId);
+		if (!form) return;
+		let element = form.element;
+		if (!element) {
+			console.warn(`Form element not found for: ${formId}`);
+			return;
+		}
+
+		form.ui.restore.container.hidden = false;
+		const handleRestore = async (changes, element) => {
+			this.isRestoring = true;
+			let theChanges = {['fields']: changes};
+			await this.checkStoredUploads(changes, element);
+			this.populate.populate(element, theChanges);
+			this.a11y.announce('Previous changes restored');
+			this.isRestoring = false;
+			form.ui.restore.container.remove();
+		};
+		const clearRestore = async (formId) => {
+			await this.checkStoredUploads(changes, element, false);
+			await this.store.delete(formId);
+			this.a11y.announce('Previous changes discarded');
+			form.ui.restore.container.remove();
+		};
+		form.ui.restore.restore.addEventListener('click', () => handleRestore(changes, element));
+		form.ui.restore.clear.addEventListener('click', async ()  => clearRestore(formId));
+	}
+		async checkStoredUploads(changes, element, restore = true) {
+			let form = this.forms.get(element.dataset.formId);
 			if (!form) return;
-			let element = form.element;
-			if (!element) {
-				console.warn(`Form element not found for: ${formId}`);
-				return;
+			let uploads = [];
+			for (let [key, value] of Object.entries(changes)) {
+				if (key.includes('_tempUpload')) {
+					let field = key.replace('_tempUpload', '');
+
+					if (Object.hasOwn(form.ui.uploads, field)) {
+						uploads = [
+							... uploads,
+							... value
+						];
+					}
+				}
 			}
 
-			const notification = document.createElement('div');
-			notification.className = 'pendingChanges';
-			notification.innerHTML = `
-			<p>We noticed unsaved changes from last time. Would you like to restore them?</p>
-        <button class="restore" data-form-id="${formId}">Restore</button>
-        <button class="discard" data-form-id="${formId}">Discard</button>`;
+			if (uploads.length > 0) {
+				if (restore) {
+					await window.jvbUploads.restoreUploads(uploads);
+				} else {
+					await window.jvbUploads.clearUploads(uploads);
+				}
 
-			element.insertBefore(notification, form.ui.status.status);
-
-			notification.querySelector('.restore').addEventListener('click', async () => {
-				this.isRestoring = true;
-
-				new this.populate(element, changes);
-				this.a11y.announce('Previous changes restored');
-
-				this.isRestoring = false;
-				notification.remove();
-			});
-
-			notification.querySelector('.discard').addEventListener('click', async () => {
-				await this.store.remove(formId);
-				this.a11y.announce('Previous changes discared');
-				notification.remove();
-			});
-
+			}
 		}
 	initValidators() {
 		this.validators = {
@@ -240,14 +281,26 @@
 	}
 	performValidation(input) {
 		const field = input.closest('.field');
-		const value = this.getFieldValue(input);
+		const value = this.getFieldCheckedValue(input);
 
 		if (!value && !input.required) {
 			return { isValid: true, message: '' };
 		}
 
-		if (input.required && !value) {
-			return { isValid: false, message: 'This field is required' };
+		if (input.required) {
+			if (input.type === 'checkbox') {
+				if (!input.checked) {
+					return { isValid: false, message: 'This field is required' };
+				}
+			} else if (input.type === 'radio') {
+				const radioGroup = document.querySelectorAll(`input[name="${input.name}"]`);
+				const anyChecked = Array.from(radioGroup).some(r => r.checked);
+				if (!anyChecked) {
+					return { isValid: false, message: 'Please select an option' };
+				}
+			} else if (!value) {
+				return { isValid: false, message: 'This field is required' };
+			}
 		}
 
 		if(input.checkValidity && !input.checkValidity()){
@@ -264,11 +317,11 @@
 		if (Object.hasOwn(field.dataset, 'validate') || input.type) {
 			const validator = this.validators[field.dataset.validate||input.type];
 
-			if (validator.pattern && !validator.pattern.test(value)) {
+			if (validator && validator.pattern && !validator.pattern.test(value)) {
 				return {isValid: false, message: validator.message};
 			}
 
-			if (validator.test) {
+			if (validator && validator.test) {
 				const result = validator.test(value, field);
 				if (result !== true) {
 					return {isValid: false, message: result};
@@ -311,10 +364,30 @@
 		if (e.target.closest('[data-ignore]') || this.isRestoring) return;
 
 		let field = this.getField(e.target);
+
+		// Check if this input lives inside a collection field
+		const collectionField = e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');
+		if (collectionField) {
+			// Dependencies still need checking
+			if (this.dependencies.has(field.dataset.field)) {
+				let dependency = this.dependencies.get(field.dataset.field);
+				dependency.forEach(item => {
+					this.checkFieldDependency(item, field.dataset.field);
+				});
+			}
+			const collectionName = collectionField.dataset.field;
+			window.debouncer.schedule(
+				`collection:${collectionName}`,
+				() => this.updateCollectionField(collectionField),
+				150
+			);
+			return;
+		}
+
 		//Dependencies
 		if (this.dependencies.has(field.dataset.field)) {
 			let dependency = this.dependencies.get(field.dataset.field);
-			dependency.items.forEach(item => {
+			dependency.forEach(item => {
 				this.checkFieldDependency(item, field.dataset.field);
 			});
 		}
@@ -333,20 +406,34 @@
 		window.debouncer.cancel(`form:${form.id}:validate:${fieldName}`);
 		this.validateField(e.target);
 
+		// If inside a collection, update the whole collection instead
+		const collectionField = e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');
+		if (collectionField) {
+			this.updateCollectionField(collectionField);
+			return;
+		}
+
 		this.updateItem(fieldName, this.getFieldValue(e.target), form);
 	}
 
 	handleInput(e){
+		if (e.target.closest('[data-ignore]') || this.isRestoring) return;
 		let form = this.getForm(e.target);
-		if (!form || !form.options.cache) return;
+		if (!form) return;
 
 		let field = this.getField(e.target);
 		if (!field) return;
 
-		this.showFormStatus(form, 'pending');
+		const input = e.target;  // Capture reference
+		const fieldName = field.dataset.field;
+
+		// Show pending status regardless of cache
+		this.showFormStatus(form.id, 'pending');
+
+		// Debounce validation
 		window.debouncer.schedule(
-			`form:${form.id}:validate:${field.dataset.field}`,
-			() => this.validateField.bind(this),
+			`form:${form.id}:validate:${fieldName}`,
+			() => this.validateField(input),
 			500
 		);
 	}
@@ -357,9 +444,12 @@
 
 		if (this.subscribers.size > 0) {
 			e.preventDefault();
-			const storedData = await this.store.get(form.id);
 
 			if (form.options.cache) {
+				this.cancelBackup();
+				await this.backup();
+				const storedData = await this.store.get(form.id);
+
 				this.notify('form-submit', {
 					config: form,
 					data: storedData.changes
@@ -375,10 +465,7 @@
 
 		if (form.options.showSummary) {
 			const storedData = await this.store.get(form.id);
-			this.showSummary(form.id, {
-				config: form,
-				data: storedData?.changes || {}
-			});
+			this.showSummary({config: form, changes: storedData?.changes});
 		}
 	}
 
@@ -389,6 +476,7 @@
 	 * @param form
 	 */
 	updateItem(name, value, form) {
+		if (value === undefined) return;
 		if (!this.changes.has(form.id)) {
 			this.changes.set(form.id, {
 				id: form.id,
@@ -398,28 +486,66 @@
 			});
 		}
 		let changes = this.changes.get(form.id);
-		changes.changes[name] = value;
+		//If it is temporary uploads, we need to store them all
+		if (name.includes('_tempUpload')) {
+			if (!Object.hasOwn(changes.changes, name)) {
+				changes.changes[name] = [];
+			}
+			changes.changes[name].push(value);
+		} else {
+			changes.changes[name] = value;
+		}
+
 		this.changes.set(form.id, changes);
 		if (form.options.cache) {
 			this.scheduleBackup();
 		}
 	}
 
-	scheduleBackup()  {
+	scheduleBackup() {
 		window.debouncer.schedule(
 			`form_changes`,
 			async () => {
 				if (this.changes.size > 0) {
-					await this.store.saveMany(this.changes);
-					for(let formId of this.changes.keys()) {
-						this.showFormStatus(formId, 'autosaved');
-					}
-					this.changes.clear();
+					await this.backup();
 				}
 			},
 			2000
 		);
 	}
+	cancelBackup() {
+		window.debouncer.cancel('form_changes');
+	}
+	async backup() {
+		// Merge with existing stored data
+		const toSave = new Map();
+
+		for (let [formId, newData] of this.changes.entries()) {
+			const stored = await this.store.get(formId);
+
+			if (stored) {
+				// Merge changes
+				toSave.set(formId, {
+					...stored,
+					...newData,
+					changes: {
+						...stored.changes,
+						...newData.changes
+					},
+					timestamp: Date.now()
+				});
+			} else {
+				toSave.set(formId, newData);
+			}
+		}
+
+		await this.store.saveMany(toSave);
+
+		for (let formId of this.changes.keys()) {
+			this.showFormStatus(formId, 'autosaved');
+		}
+		this.changes.clear();
+	}
 
 	saveCache(formId) {
 		if (!this.changes.has(formId)) return;
@@ -436,6 +562,17 @@
 	 * @param {object} options
 	 */
 	registerForm(form, options) {
+		options = {
+			autoUpload: false,
+			imageMeta: true,
+			delay: 1500,
+			endpoint: Object.hasOwn(form.dataset, 'save') ? form.dataset.save: '',
+			showStatus: true,
+			showSummary: false,
+			cache: true,
+			ignore: [],
+			... options
+		};
 		//Bail if form already registered
 		if (Object.hasOwn(form.dataset, 'formId') && this.forms.has(form.dataset.formId)) return;
 
@@ -450,696 +587,808 @@
 			element: form,
 			id: formId,
 			status: '',
-			options: {
-				autoUpload: false,
-				delay: options.delay??1500,
-				endpoint: options.save??form.dataset.save??'',
-				formStatus: options.showStatus??true,
-				showSummary: false,
-				cache: options.cache??true,
-			},
+			options: options,
 			ui: window.uiFromSelectors(this.selectors.forms, form)
 		};
 
-		if (config.showSummary && !this.summaryTemplate) {
-			this.defineSummaryTemplate();
-		}
+		config.ui.fields = {};
+		form.querySelectorAll('[data-field]').forEach((field) => {
+			config.ui.fields[field.dataset.field] = field;
+		});
 
 		this.initializeFields(form, config);
 		this.forms.set(formId, config);
 
 		return config;
 	}
-		clearForm(formId) {
-			const config = this.forms.get(formId);
-			if (!config) return;
+	clearForm(formId) {
+		const config = this.forms.get(formId);
+		if (!config) return;
 
-			if (config.unsubscribeTabs) {
-				config.unsubscribeTabs();
+		if (config.unsubscribeTabs) {
+			config.unsubscribeTabs();
+		}
+		if(config.tabs) {
+			window.jvbTabs.removeTab(config.element);
+		}
+
+		if (config.cache && this.changes.has(formId)) this.saveCache(formId);
+
+		// Cleanup items
+		for (let [id, input] of this.inputs.entries()) {
+			if (input.form === formId) {
+				this.inputs.delete(id);
 			}
-			if(config.tabs) {
-				window.jvbTabs.removeTab(config.element);
+		}
+		// Clean up dependencies for this form
+		this.dependencies.forEach((dependency, fieldName) => {
+			dependency = dependency.filter(item => item.form !== formId);
+
+			// Remove the dependency entry entirely if no items left
+			if (dependency.length === 0) {
+				this.dependencies.delete(fieldName);
 			}
+		});
 
-			if (config.cache && this.changes.has(formId)) this.saveCache(formId);
+		if (Object.hasOwn(config, 'hasQuill') && this.quillInstances.has(formId)) {
+			const instances = this.quillInstances.get(formId);
+			instances.forEach(quillInstance => {
+				// Disable the editor
+				quillInstance.disable();
 
-			// Cleanup items
-			for (let [id, input] of this.inputs.entries()) {
-				if (input.form === formId) {
-					this.inputs.delete(id);
+				// Remove all event listeners
+				quillInstance.off('text-change');
+				quillInstance.off('selection-change');
+
+				// Get the container elements
+				const container = quillInstance.container.parentElement;
+				const toolbar = container?.querySelector('.ql-toolbar');
+
+				// Remove toolbar
+				if (toolbar) {
+					toolbar.remove();
 				}
-			}
-			// Clean up dependencies for this form
-			this.dependencies.forEach((dependency, fieldName) => {
-				dependency.items = dependency.items.filter(item => item.form !== formId);
 
-				// Remove the dependency entry entirely if no items left
-				if (dependency.items.length === 0) {
-					this.dependencies.delete(fieldName);
+				// Clear the editor content
+				quillInstance.setText('');
+
+				// Remove container
+				if (container && container.classList.contains('editor-container')) {
+					const textarea = container.nextElementSibling;
+					if (textarea?.tagName === 'TEXTAREA') {
+						textarea.style.display = '';
+					}
+					container.remove();
 				}
 			});
 
-			if (Object.hasOwn(config, 'hasQuill') && this.quillInstances.has(formId)) {
-				const instances = this.quillInstances.get(formId);
-				instances.forEach(quillInstance => {
-					// Disable the editor
-					quillInstance.disable();
-
-					// Remove all event listeners
-					quillInstance.off('text-change');
-					quillInstance.off('selection-change');
-
-					// Get the container elements
-					const container = quillInstance.container.parentElement;
-					const toolbar = container?.querySelector('.ql-toolbar');
-
-					// Remove toolbar
-					if (toolbar) {
-						toolbar.remove();
+			this.quillInstances.delete(formId);
+		}
+		let checks = {
+			repeater: this.repeaters,
+			tagList: this.tagLists,
+			charLimit: this.charLimits,
+			quantity: this.quantityFields
+		};
+		for (let [type, check] of Object.entries(checks)) {
+			if (check.size === 0) continue;
+			let hasAny = Array.from(check.values()).filter(item => item.form === formId);
+			if (hasAny.length > 0) {
+				hasAny.forEach(item => {
+					switch (type) {
+						case 'repeater':
+							this.removeRepeaterListeners(item.element);
+							break;
+						case 'tagList':
+							this.removeTagListListeners(item.element);
+							break;
+						case 'charLimit':
+							this.removeCharacterLimitListeners(item.element);
+							break;
+						case 'quantity':
+							this.removeQuantityListeners(item.element);
+							break;
 					}
 
-					// Clear the editor content
-					quillInstance.setText('');
-
-					// Remove container
-					if (container && container.classList.contains('editor-container')) {
-						const textarea = container.nextElementSibling;
-						if (textarea?.tagName === 'TEXTAREA') {
-							textarea.style.display = '';
-						}
-						container.remove();
+					if (check.has(item.id)) {
+						check.delete(item.id);
 					}
 				});
-
-				this.quillInstances.delete(formId);
 			}
-			let checks = {
-				repeater: this.repeaters,
-				tagList: this.tagLists,
-				charLimit: this.charLimits,
-				quantity: this.quantityFields
-			};
-			for (let [type, check] of Object.entries(checks)) {
-				if (check.size === 0) continue;
-				let hasAny = Array.from(check.values()).filter(item => item.form === formId);
-				if (hasAny.length > 0) {
-					hasAny.forEach(item => {
-						switch (type) {
-							case 'repeater':
-								this.removeRepeaterListeners(item.element);
-								break;
-							case 'tagList':
-								this.removeTagListListeners(item.element);
-								break;
-							case 'charLimit':
-								this.removeCharacterLimitListeners(item.element);
-								break;
-							case 'quantity':
-								this.removeQuantityListeners(item.element);
-								break;
+		}
+
+
+		this.removeFormListeners(config.element);
+		this.forms.delete(formId);
+
+		window.debouncer.cancel(`form_changes`);
+	}
+	defineSummaryTemplate() {
+		this.summaryTemplate = true;
+		let form = this;
+		this.templates.define(
+			'formSummary',
+			{
+				refs: {
+					result: '.result',
+					h3: 'h3',
+					p: 'p',
+				},
+				setup({ el, refs, manyRefs, data }) {
+					const skipFields = ['sendAll', ...data.config.options.ignore??[]];
+
+					for (let [key, value] of Object.entries(data.changes)) {
+						if (skipFields.includes(key) || form.isEmptyValue(value)) continue;
+
+						let input = Array.from(form.inputs.values())
+							.find(temp => temp.field?.dataset.field === key);
+						if (!input) continue;
+
+						let entry = refs.result.cloneNode(true);
+						let title = entry.querySelector('h3');
+						let p = entry.querySelector('p');
+
+						// Get field label - prioritize legend for fieldsets, then label
+						const legend = input.field?.querySelector('legend');
+						title.textContent = legend
+							? legend.textContent.replace('*', '').trim()
+							: input.ui.label?.textContent.replace('*', '').trim();
+
+
+						const formattedValue = form.formatValueForSummary(value, input);
+
+						if (formattedValue instanceof HTMLElement) {
+							// If it's an HTML element (repeater, tag-list, etc.), replace <p>
+							p.replaceWith(formattedValue);
+						} else {
+							// If it's a string, set text content
+							p.textContent = formattedValue;
 						}
-					});
-					check.delete(item.id);
+
+						el.append(entry);
+					}
+					let uploads = data.config?.element?.querySelectorAll('[data-upload-field]');
+					if (uploads) {
+						uploads.forEach(upload => {
+							let label = upload.querySelector('h2')?.textContent??'Upload:';
+							let imgs = upload.querySelectorAll('.item-grid.preview img');
+							let field = refs.result.cloneNode(true);
+							if (imgs) {
+								let entry = refs.result.cloneNode(true);
+								let title = field.querySelector('h3');
+								let p = field.querySelector('p');
+								p?.remove();
+								if (title) title.textContent = label;
+								imgs.forEach(img => {
+									img = img.cloneNode(true);
+									entry.append(img);
+								});
+								el.append(entry);
+							}
+						});
+					}
+
+					refs.result?.remove();
+					data.config.element.after(el);
+					window.fade(data.config.element, false);
 				}
 			}
+		);
+	}
 
 
-			this.removeFormListeners(config.element);
-			this.forms.delete(formId);
+	initializeFields(container, config = null) {
+		const fieldHandlers = {
+			'[data-editor]': () => this.checkForQuill(container,config),
+			'div.quantity': () => this.checkForQuantity(container),
+			'.repeater': () => this.checkForRepeaters(container, config),
+			'.field.tag-list': () => this.checkForTagLists(container),
+			'[data-depends-on]': () => this.checkForConditionalFields(container),
+			'[data-limit]': () => this.checkForCharacterLimits(container),
+			'[data-uploader],[data-upload-field]': () => this.checkForImageUploads(container, config),
+			'nav.tabs': () => this.checkForTabs(container, config),
+			'[data-type="selector"]': () => this.checkForSelectors(container)
+		};
 
-			window.debouncer.cancel(`form_changes`);
+		for (const [selector, handler] of Object.entries(fieldHandlers)) {
+			if (container.querySelector(selector)) {
+				handler();
+			}
 		}
-		defineSummaryTemplate() {
-			this.summaryTemplate = true;
-			let form = this;
+
+		let inputs = Array.from(container.querySelectorAll(this.inputSelectors))
+			.filter(input => !input.closest('.ql-clipboard'));
+		inputs.map(input => {
+			this.getItem(input, config?.id);
+		});
+	}
+	checkForQuill(form, config) {
+		if (!form.querySelector('[data-editor]')) return;
+		if (config && !Object.hasOwn(config, 'hasQuill')){
+			config.hasQuill = true;
+			this.forms.set(config.id, config);
+		}
+
+		if (!this.quillInstances.has(config.id)) {
+			this.quillInstances.set(config.id, new Set());
+		}
+
+		const instances = window.jvbQuill(form);
+		instances.forEach(instance => {
+			this.quillInstances.get(config.id).add(instance);
+		});
+	}
+	checkForQuantity(form) {
+		if (!form.querySelector(this.selectors.number.number)) return;
+		form.querySelectorAll(this.selectors.number.number).forEach(num => {
+			let config = {
+				id: window.generateID('quant'),
+				form: form.dataset.formId,
+				ui: window.uiFromSelectors(this.selectors.number, num),
+				element: num
+			};
+			num.dataset.numId = config.id;
+			this.quantityFields.set(config.id, config);
+			this.addQuantityListeners(num);
+		});
+	}
+	addQuantityListeners(el) {
+		el.addEventListener('click', this.quantityClick);
+	}
+	removeQuantityListeners(el) {
+		el.removeEventListener('click', this.quantityClick);
+	}
+	handleQuantityClick(e) {
+		let conf = this.quantityFields.get(e.target.closest('[data-num-id]')?.dataset.numId);
+		if(!conf) return;
+		let change = 0;
+		if (conf.ui.increase.contains(e.target)) {
+			change++;
+		} else if (conf.ui.decrease.contains(e.target)) {
+			change--;
+		}
+		if (change === 0) return;
+		let field = this.getField(e.target);
+		let step = conf.ui.input.step;
+		step = Math.max(step, 1);
+		if (e.ctrlKey && e.shiftKey) {
+			step = step * 50;
+		} else if (e.ctrlKey) {
+			step = step *5;
+		} else if (e.shiftKey) {
+			step = step * 10;
+		}
+		let value = (conf.ui.input.value === '') ? 0 : parseFloat(conf.ui.input.value);
+		conf.ui.input.value = (value + (step * change));
+
+		value = parseFloat(conf.ui.input.value);
+
+		if (conf.ui.input.min && value < conf.ui.input.min) {
+			conf.ui.input.value = conf.ui.input.min;
+			conf.ui.decrease.disabled = true;
+		} else if (conf.ui.input.max && value > conf.ui.input.max) {
+			conf.ui.input.value = conf.ui.input.max;
+			conf.ui.increase.disabled = true;
+		} else {
+			if (conf.ui.decrease.disabled) conf.ui.decrease.disabled = false;
+			if (conf.ui.increase.disabled) conf.ui.increase.disabled = false;
+		}
+	}
+	checkForRepeaters(form) {
+
+		if (!form.querySelector(this.selectors.repeater.repeater)) return;
+
+		form.querySelectorAll(this.selectors.repeater.repeater).forEach(repeater => {
+			let config = {
+				id: repeater.querySelector('template').className??window.generateID('repeater'),
+				ui: window.uiFromSelectors(this.selectors.repeater, repeater),
+				form: form.dataset.formId,
+				element: repeater,
+				field: this.getField(repeater),
+				sortable: false,
+				rows: []
+			};
+
+			if (!config.ui.add) return;
+
+			let template = repeater.querySelector('template');
 			this.templates.define(
-				'formSummary',
+				template.className,
+				{
+					manyRefs: {
+						inputs: this.inputSelectors,
+					},
+					setup({el, refs, manyRefs, data}) {
+						let index = config.ui.items?.children?.length??0;
+						el.dataset.index = index;
+
+						manyRefs.inputs?.forEach(input => {
+							window.prefixInput(input, `${data.repeater.dataset.field}:${index}:`, el, false, true);
+						});
+					}
+				},
+			);
+
+			if (window.Sortable) {
+				config.sortable = new Sortable(repeater, {
+					handle: this.selectors.repeater.header,
+					animation: 150,
+					onEnd: () => {
+						this.reindexList(repeater);
+					}
+				});
+			}
+
+			repeater.dataset.repeaterId = config.id;
+			this.addRepeaterListeners(repeater);
+			this.repeaters.set(config.id, config);
+		});
+
+	}
+	addRepeaterListeners(el) {
+		el.addEventListener('click', this.repeaterClick);
+	}
+	removeRepeaterListeners(el) {
+		el.removeEventListener('click', this.repeaterClick);
+	}
+	handleRepeaterClick(e) {
+		if (e.target.matches(this.selectors.repeater.add)) {
+			this.addRepeaterRow(e.target.closest('[data-repeater-id]'));
+		} else if (e.target.matches(this.selectors.repeater.remove)) {
+			this.removeRepeaterRow(e.target.closest('[data-index]'));
+		}
+	}
+	addRepeaterRow(repeater) {
+		let data = {};
+		data.repeater = repeater;
+		let config = this.repeaters.get(repeater.dataset.repeaterId);
+
+		let row = this.templates.create(repeater.dataset.repeaterId, data);
+		config.rows.push({
+			element: row,
+			fields: Array.from(row.querySelectorAll('[data-field]'))
+		});
+		this.repeaters.set(config.id, config);
+		config.ui.items.append(row);
+
+		let form = this.getForm(repeater);
+		this.initializeFields(repeater, form);
+		this.a11y.announce('Row added');
+	}
+	removeRepeaterRow(row) {
+		let repeater = row.closest('[data-repeater-id]');
+		row.remove();
+		this.reindexList(repeater);
+		this.a11y.announce('Row removed');
+	}
+	checkForTagLists(form) {
+		form.querySelectorAll(this.selectors.tagList.tagList)?.forEach(field=> {
+			let config = {
+				id: field.querySelector('template').className??window.generateID('tagList'),
+				ui: window.uiFromSelectors(this.selectors.tagList, field),
+				element: field,
+				form: form.dataset.formId,
+				format: field.dataset.tagFormat??'first_field'
+			};
+			if (!config.ui.input || !config.ui.add || !config.ui.items) return;
+
+			field.dataset.tagListId = config.id;
+			config.fieldName = field.dataset.field;
+
+			let template = field.querySelector('template');
+			this.templates.define(
+				template.className,
 				{
 					refs: {
-						result: '.result',
-						h3: 'h3',
-						p: 'p',
+						label: this.selectors.tagList.label,
 					},
-					setup({ el, refs, manyRefs, data }) {
-						const skipFields = ['sendAll', ...form.ignore];
+					manyRefs: {
+						inputs: this.inputSelectors,
+					},
+					setup({el, refs, manyRefs, data}) {
+						let index = config.ui.items?.children?.length??0;
+						el.dataset.index = index;
+						manyRefs.inputs?.forEach(input => {
+							let wrapper = input.closest('.tag-item');
+							window.prefixInput(input, `${data.fieldName}:${index}:`, wrapper, false, true)
+						});
 
-						for (let [key, value] of Object.entries(data.changes)) {
-							if (skipFields.includes(key) || this.isEmptyValue(value)) continue;
-
-							let input = Array.from(this.inputs.values())
-								.find(temp => temp.field?.dataset.field === key);
-							if (!input) continue;
-
-							let entry = refs.result.cloneNode(true);
-							let title = entry.querySelector('h3');
-							let p = entry.querySelector('p');
-
-							title.textContent = input.label.textContent;
-
-							if (typeof value === 'string') {
-								p.textContent = value;
-							} else if (Array.isArray(value)) {
-								//Repeater or Tag Item
-							} else if (typeof value === 'object') {
-								//Location item
-								p.textContent = `${value.address}`;
-							}
-
-							el.append(entry);
+						if (refs.label) {
+							refs.label.textContent = data.label;
 						}
-						let uploads = data.config?.element?.querySelectorAll('[data-upload-field]');
-						if (uploads) {
-							uploads.forEach(upload => {
-								let label = upload.querySelector('h2')?.textContent??'Upload:';
-								let imgs = upload.querySelectorAll('.item-grid.preview img');
-								if (imgs) {
-									let entry = refs.result.cloneNode(true);
-									let title = field.querySelector('h3');
-									let p = field.querySelector('p');
-									p?.remove();
-									if (title) title.textContent = label;
-									imgs.forEach(img => {
-										img = img.cloneNode(true);
-										entry.append(img);
-									});
-									el.append(entry);
-								}
-							});
-						}
-
-						refs.result?.remove();
-						data.config.element.after(el);
-						window.fade(data.config.element, false);
 					}
-				}
+				},
 			);
+			config.ui.inputs = Array.from(field.querySelectorAll(this.selectors.tagList.inputs));
+			config.ui.value = Array.from(field.querySelectorAll(this.selectors.tagList.value));
+			this.tagLists.set(config.id, config);
+			this.addTagListListeners(field);
+		});
+
+	}
+	addTagListListeners(el) {
+		el.addEventListener('click', this.tagListClick);
+		el.addEventListener('keypress', this.tagListInput);
+	}
+	removeTagListListeners(el) {
+		el.removeEventListener('click', this.tagListClick);
+		el.removeEventListener('keypress', this.tagListInput);
+	}
+
+	handleTagListClick(e) {
+		if (window.targetCheck(e,this.selectors.tagList.add)) {
+			this.addTagListItem(e.target.closest('[data-tag-list-id]'));
+		} else if (window.targetCheck(e, this.selectors.tagList.remove)) {
+			this.removeTagListItem(e.target.closest(this.selectors.tagList.item));
 		}
-		initializeFields(container, config = null) {
-			const fieldHandlers = {
-				'[data-editor]': () => this.checkForQuill(container,config),
-				'div.quantity': () => this.checkForQuantity(container),
-				'.repeater': () => this.checkForRepeaters(container, config),
-				'.field.tag-list': () => this.checkForTagLists(container),
-				'[data-depends-on]': () => this.checkForConditionalFields(container),
-				'[data-limit]': () => this.checkForCharacterLimits(container),
-				'[data-uploader]': () => this.checkForImageUploads(container, config),
-				'nav.tabs': () => this.checkForTabs(container, config),
-				'[data-type="selector"]': () => this.checkForSelectors(container)
+	}
+	addTagListItem(tagList) {
+		let config = this.tagLists.get(tagList.dataset.tagListId);
+		if (!config) return;
+
+		let data = {};
+		let hasValue = false;
+		let isValid = true;
+
+		// First pass: validate all inputs
+		for (let input of config.ui.inputs) {
+			const isRequired = input.required || input.dataset.required === 'true';
+			const value = this.getFieldValue(input);
+
+			if (value) hasValue = true;
+
+			// Validate and check for errors
+			const valid = this.validateField(input);
+
+			if (isRequired && !value) {
+				this.showError(input, 'This field is required');
+				isValid = false;
+			} else if (!valid) {
+				isValid = false;
+			}
+
+			const fieldName = input.name.replace('new_','');
+			data[fieldName] = value;
+		}
+
+		// Stop if validation failed
+		if (!isValid) {
+			this.a11y.announce('Please correct the errors before adding');
+			const firstInvalid = config.ui.inputs.find(input => {
+				const isRequired = input.required || input.dataset.required === 'true';
+				return (isRequired && !this.getFieldValue(input));
+			});
+			if (firstInvalid) firstInvalid.focus();
+			return;
+		}
+
+		if (!hasValue) {
+			this.a11y.announce('Please fill in at least one field');
+			config.ui.inputs[0].focus();
+			return;
+		}
+
+		// Build label
+		let label;
+		switch (config.format) {
+			case 'first_field':
+				label = Object.values(data)[0];
+				break;
+			case 'all_fields':
+				label = Object.values(data).join(', ');
+				break;
+			default:
+				if (config.format.includes('{')) {
+					label = config.format;
+					for (const [key, value] of Object.entries(data)) {
+						label = label.replace(`{${key}}`, value);
+					}
+				} else {
+					label = data[config.format]??Object.values(data)[0];
+				}
+				break;
+		}
+
+		let newItem = this.templates.create(tagList.dataset.tagListId, {
+			label: label,
+			fieldName: config.fieldName
+		});
+
+		const index = config.ui.items?.children?.length ?? 0;
+		newItem?.querySelectorAll('input[type=hidden]')?.forEach(input => {
+			const fieldKey = input.dataset.field;
+			input.name = `${config.fieldName}:${index}:${fieldKey}`;
+			input.id = `${config.fieldName}:${index}:${fieldKey}`;
+			input.value = data[fieldKey] || '';
+		});
+
+		config.ui.items.append(newItem);
+
+		// Clear inputs AFTER success
+		for (let input of config.ui.inputs) {
+			if (['checkbox', 'radio'].includes(input.type)) {
+				input.checked = false;
+			} else {
+				input.value = '';
+			}
+			this.clearValidation(input);
+		}
+
+		config.ui.inputs[0]?.focus();
+		this.updateCollectionField(tagList);
+		this.a11y.announce('Item added');
+	}
+	removeTagListItem(item) {
+		let tagList = item.closest('[data-tag-list-id]');
+		if (!tagList) return;
+		item.remove();
+		this.reindexList(tagList);
+		this.updateCollectionField(tagList);
+		this.a11y.announce('Item removed');
+	}
+	handleTagListInput(e) {
+		let target = e.target;
+		let field = target.closest('[data-tag-list-id]');
+		if (!field) return;
+		let config = this.tagLists.get(field.dataset.tagListId);
+		if (!config) return;
+
+		if (e.key === 'Enter') {
+			if (target === config.ui.inputs[config.ui.inputs.length - 1]) {
+				e.preventDefault();
+				this.addTagListItem(target.closest('[data-tag-list-id]'));
+			} else {
+				e.preventDefault();
+				let index = config.ui.inputs.indexOf(target);
+				config.ui.inputs[index+1].focus();
+			}
+		}
+
+	}
+
+	checkForConditionalFields(form) {
+		form.querySelectorAll(this.selectors.dependsOn).forEach( field => {
+			const dependsOn = field.dataset.dependsOn;
+			const requiredValue = field.dataset.dependsValue;
+			const operator = field.dataset.dependsOperatior??'==';
+
+			let formData = this.forms.get(form.dataset.formId);
+
+			if (!this.dependencies.has(dependsOn)) {
+				if (Object.hasOwn(formData.ui.fields, dependsOn)) {
+					this.dependencies.set(dependsOn, []);
+				}
+			}
+			let dependency = this.dependencies.get(dependsOn);
+			if (dependency) {
+				dependency.push({
+					field:	field,
+					form: form.dataset.formId,
+					requiredValue: requiredValue,
+					operator: operator
+				});
+				this.dependencies.set(dependsOn, dependency);
+			}
+
+			this.checkFieldDependency(field, dependsOn);
+		});
+	}
+	checkFieldDependency(dependentField, controlFieldName) {
+		const form = this.getForm(dependentField);
+		const controlField = this.dependencies.get(controlFieldName);
+		if (!controlField) return;
+
+
+		const controlValue = this.getFieldValue(form.ui.fields[controlFieldName]);
+		const shouldShow = this.evaluateCondition(
+			controlValue,
+			dependentField.dataset.dependsValue,
+			dependentField.dataset.dependsOperatior
+		);
+
+		this.toggleFieldVisibility(dependentField, shouldShow);
+	}
+	evaluateCondition(value, requiredValue, operator) {
+		const fieldStr = String(value || '');
+		const requiredStr = String(requiredValue || '');
+
+		switch (operator) {
+			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);
+			case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
+			case 'contains': return fieldStr.includes(requiredStr);
+			case 'empty': return fieldStr === '';
+			case 'not_empty': return fieldStr !== '';
+			default: return fieldStr === requiredStr;
+		}
+	}
+	toggleFieldVisibility(field, show) {
+		const wrapper = field.closest('.field, fieldset');
+		if (!wrapper) return;
+
+		wrapper.hidden = !show;
+		wrapper.querySelectorAll('input, select, textarea').forEach(control => {
+			control.disabled = !show;
+			if (!show && control.hasAttribute('required')) {
+				control.dataset.wasRequired = 'true';
+				control.removeAttribute('required');
+			} else if (show && control.dataset.wasRequired === 'true') {
+				control.setAttribute('required', '');
+				delete control.dataset.wasRequired;
+			}
+		});
+	}
+	checkForCharacterLimits(form) {
+		if (!form.querySelector(this.selectors.limits.hasLimit)) return;
+		this.countUpdaters = this.updateCount.bind(this);
+
+		form.querySelectorAll(this.selectors.limits.hasLimit).forEach(field => {
+			const input = this.getFieldInput(field);
+			if (!input) return;
+
+			let id = window.generateID('limit');
+			input.dataset.charLimitId = id;
+			input.dataset.limit = field.dataset.maxlength;
+
+			let config = {
+				element: input,
+				form: form.dataset.formId,
+				ui: window.uiFromSelectors(this.selectors.limits, field)
 			};
 
-			for (const [selector, handler] of Object.entries(fieldHandlers)) {
-				if (container.querySelector(selector)) {
-					handler();
-				}
+			if (config.ui.limit) {
+				config.ui.limit.textContent = field.dataset.maxlength;
 			}
 
-			let inputs = Array.from(container.querySelectorAll(this.inputSelectors));
-			inputs.map(input => {
-				this.getItem(input, config?.id);
+			this.charLimits.set(id, config);
+			this.addCharacterLimitListeners(input);
+		});
+	}
+	addCharacterLimitListeners(input) {
+		input.addEventListener('input', this.countUpdaters, {passive: true});
+	}
+	removeCharacterLimitListeners(input) {
+		input.removeEventListener('input', this.countUpdaters, {passive: true});
+	}
+	updateCount(e) {
+		let target = e.target;
+		let config = this.charLimits.get(target.dataset.charLimitId);
+		if (!config) return;
+		let length = target.value.length;
+		let limit = target.dataset.limit;
+		if (config.ui.current) {
+			config.ui.current.textContent = length;
+			config.ui.current.classList.toggle('exceeded', length >= limit);
+		}
+		if (length > limit) {
+			target.value = target.value.slice(0, limit);
+		}
+	}
+	checkForImageUploads(form, config) {
+		this.hasUploads = true;
+		window.jvbUploads.scanFields(form, config.options.autoUpload, config.options.imageMeta);
+		let uploads = form.querySelectorAll('[data-field-type="upload"]');
+		if (uploads) {
+			config.ui.uploads = {};
+			uploads.forEach(upload => {
+				config.ui.uploads[upload.dataset.field] = upload;
 			});
 		}
-			checkForQuill(form, config) {
-				if (!form.querySelector('[data-editor]')) return;
-				if (config && !Object.hasOwn(config, 'hasQuill')){
-					config.hasQuill = true;
-					this.forms.set(config.id, config);
-				}
+	}
 
-				if (!this.quillInstances.has(config.id)) {
-					this.quillInstances.set(config.id, new Set());
+	checkForTabs(form, config) {
+		if (window.jvbTabs && form.querySelector('nav.tabs')) {
+			config.tabs = window.jvbTabs.registerTab(form, {
+				preCheck: (section, tabConfig) => {
+					return this.validateStep(section, config);
 				}
+			});
+			config.ui.tabs = window.uiFromSelectors(this.selectors.tabs, form);
+			config.ui.tabs.sections = Array.from(form.querySelectorAll(this.selectors.tabs.sections));
+			config.ui.tabs.inputs = {};
+			config.ui.tabs.sections.forEach(section => {
+				config.ui.tabs.inputs[section.dataset.tab] = Array.from(section.querySelectorAll(this.inputs));
+			});
+			config.ui.tabs.buttons = Array.from(form.querySelectorAll(this.selectors.tabs.buttons));
 
-				const instances = window.jvbQuill(form);
-				instances.forEach(instance => {
-					this.quillInstances.get(config.id).add(instance);
-				});
-			}
-			checkForQuantity(form) {
-				if (!form.querySelector(this.selectors.number.number)) return;
-				form.querySelectorAll(this.selectors.number.number).forEach(num => {
-					let config = {
-						id: window.generateID('quant'),
-						form: form.dataset.formId,
-						ui: window.uiFromSelectors(this.selectors.number, num),
-						element: num
-					};
-					num.dataset.numId = config.id;
-					this.quantityFields.set(config.id, config);
-					this.addQuantityListeners(num);
-				});
-			}
-				addQuantityListeners(el) {
-					el.addEventListener('click', this.quantityClick);
-				}
-				removeQuantityListeners(el) {
-					el.removeEventListener('click', this.quantityClick);
-				}
-				handleQuantityClick(e) {
-					let conf = this.quantityFields.get(e.target.closest('[data-num-id]')?.dataset.numId);
-					if(!conf) return;
-					let change = 0;
-					if (conf.increase.contains(e.target)) {
-						change++;
-					} else if (conf.decrease.contains(e.target)) {
-						change--;
-					}
-					if (change === 0) return;
-					let field = this.getField(e.target);
-					let step = conf.input.step;
-					step = Math.max(step, 1);
-					if (e.ctrlKey && e.shiftKey) {
-						step = step * 50;
-					} else if (e.ctrlKey) {
-						step = step *5;
-					} else if (e.shiftKey) {
-						step = step * 10;
-					}
-					let value = (conf.input.value === '') ? 0 : parseFloat(conf.input.value);
-					conf.input.value = (value + (step * change));
+			config.unsubscribeTabs = window.jvbTabs.subscribe((event, data) => {
+				if (event === 'tab-switched') {
+					if (config.ui.tabs.progress) {
+						const section = config.ui.tabs.sections.filter(section => section.dataset.tab === data.current)[0]??false;
+						if (!section) return;
+						const step = section.dataset.step;
+						const total = config.ui.sections.length;
 
-					value = parseFloat(conf.input.value);
-
-					if (conf.input.min && value < conf.input.min) {
-						conf.input.value = conf.input.min;
-						conf.decrease.disabled = true;
-					} else if (conf.input.max && value > conf.input.max) {
-						conf.input.value = conf.input.max;
-						conf.increase.disabled = true;
-					} else {
-						if (conf.decrease.disabled) conf.decrease.disabled = false;
-						if (conf.increase.disabled) conf.increase.disabled = false;
+						window.showProgress(
+							config.ui.tabs.progress,
+							step,
+							total
+						);
 					}
 				}
-			checkForRepeaters(form) {
-				if (!form.querySelector(this.selectors.repeater.repeater)) return;
+			});
+			this.forms.set(config.id, config);
+		}
+	}
+	validateStep(section, config) {
+		const formId = section.closest('[data-form-id]')?.dataset.formId;
+		if (!formId) return true;
 
-				form.querySelectorAll(this.selectors.repeater.repeater).forEach(repeater => {
-					let config = {
-						id: repeater.querySelector('template').className??window.generateID('repeater'),
-						ui: window.uiFromSelectors(this.selectors.repeater, repeater),
-						form: form.dataset.formId,
-						element: repeater,
-						field: this.getField(repeater),
-						sortable: false,
-					};
+		const form = this.forms.get(formId);
+		if (!form) return true;
 
-					if (!config.ui.addButton) return;
+		const inputs = Array.from(this.inputs.values())
+			.filter(item =>
+				item &&
+				item.form === formId &&
+				item.section === section.dataset.tab &&
+				!item.element.closest('[hidden]')
+			);
 
-					let template = repeater.querySelector('template');
-					this.templates.define(
-						template.className,
-						{
-							manyRefs: {
-								inputs: this.inputSelectors,
-							},
-							setup({el, refs, manyRefs, data}) {
-								let index = config.ui.items?.children?.length??0;
-								el.dataset.index = index;
-								manyRefs.inputs?.forEach(input => {
-									window.prefixInput(input, `${el.dataset.fieldName}:${index}:`)
-								});
-							}
-						},
-					);
-
-					if (window.Sortable) {
-						config.sortable = new Sortable(repeater, {
-							handle: this.selectors.repeater.header,
-							animation: 150,
-							onEnd: () => {
-								this.reindexList(repeater);
-							}
-						});
-					}
-					repeater.dataset.repeaterId = config.id;
-					this.addRepeaterListeners(repeater);
-					this.repeaters.set(config.id, config);
-				});
-
-			}
-				addRepeaterListeners(el) {
-					el.addEventListener('click', this.repeaterClick);
-				}
-				removeRepeaterListeners(el) {
-					el.removeEventListener('click', this.repeaterClick);
-				}
-				handleRepeaterClick(e) {
-					if (e.target.matches(this.selectors.repeater.add)) {
-						this.addRepeaterRow(e.target.closest('[data-repeater-id]'));
-					} else if (e.target.matches(this.selectors.repeater.remove)) {
-						this.removeRepeaterRow(e.target);
-					}
-				}
-				addRepeaterRow(repeater) {
-					repeater.append(this.templates.create(repeater.dataset.repeaterId));
-					this.a11y.announce('Row added');
-				}
-				removeRepeaterRow(row) {
-					let repeater = row.closest('[data-repeater-id]');
-					row.remove();
-					this.reindexList(repeater);
-					this.a11y.announce('Row removed');
-				}
-			checkForTagLists(form) {
-				form.querySelectorAll(this.selectors.tagList.tagList)?.forEach(field=> {
-					let config = {
-						id: field.querySelector('template').className??window.generateID('tagList'),
-						ui: window.uiFromSelectors(this.selectors.tagList, field),
-						element: field,
-						form: form.dataset.formId,
-						format: field.dataset.tagFormat??'first_field'
-					};
-					if (!config.ui.input || !config.ui.add || !config.ui.items) return;
-
-					field.dataset.tagListId = config.id;
-
-					let template = field.querySelector('template');
-					this.templates.define(
-						template.className,
-						{
-							refs: {
-								label: this.selectors.tagList.label,
-							},
-							manyRefs: {
-								inputs: this.inputSelectors,
-							},
-							setup({el, refs, manyRefs, data}) {
-								let index = config.ui.items?.children?.length??0;
-								el.dataset.index = index;
-								manyRefs.inputs?.forEach(input => {
-									window.prefixInput(input, `${el.dataset.fieldName}:${index}:`)
-								});
-
-								if (refs.label) {
-									refs.label.textContent = data.label;
-								}
-							}
-						},
-					);
-
-					this.tagLists.set(config.id, config);
-					this.addTagListListeners(field);
-				});
-
-			}
-				addTagListListeners(el) {
-					el.addEventListener('click', this.tagListClick);
-					el.addEventListener('keypress', this.tagListInput, {passive: true})
-				}
-				removeTagListListeners(el) {
-					el.removeEventListener('click', this.tagListClick);
-					el.removeEventListener('keypress', this.tagListInput)
-				}
-
-				handleTagListClick(e) {
-					if (e.target.matches(this.selectors.tagList.add)) {
-						this.addTagListItem(e.target.closest('[data-tag-list-id]'));
-					} else if (e.target.matches(this.selectors.tagList.remove)) {
-						this.removeTagListItem(e.target.closest(this.selectors.tagList.remove));
-					}
-				}
-					addTagListItem(tagList) {
-						let config = this.tagLists.get(tagList.dataset.tagListId);
-						if (!config) return;
-
-						let data = {};
-						let hasValue = false;
-
-						for (let input of config.ui.inputs) {
-							this.validateField(input);
-							const fieldName = input.name.replace('new_','');
-							const value = this.getFieldValue(input);
-							if (value) hasValue = true;
-							data[fieldName] = value;
-
-							//clear values and validation
-							if (['checkbox', 'radio'].includes(input.type)) {
-								input.checked = false;
-							} else {
-								input.value = '';
-							}
-							this.clearValidation(input);
-						}
-
-						if (!hasValue) {
-							this.a11y.announce('Please fill in at least one field');
-							config.ui.inputs[0].focus();
-							return;
-						}
-
-						let label;
-						switch (config.format) {
-							case 'first_field':
-								label = Object.values(data)[0];
-								break;
-							case 'all_fields':
-								label = Object.values(data).join(', ');
-								break;
-							default:
-								if (format.includes('{')) {
-									let label = config.format;
-									for (const [key, value] of Object.entries(data)) {
-										label = label.replace(`{${key}}`, value);
-									}
-								} else {
-									label = data[config.format]??Object.values(data)[0];
-								}
-								break;
-						}
-
-						let newItem = this.templates.create(tagList.dataset.tagListId, {
-							label: label
-						});
-
-						const index = config.ui.items?.children?.length ?? 0;
-						newItem?.querySelectorAll('input[type=hidden]')?.forEach(input => {
-							const fieldKey = input.dataset.field;
-							input.name = `${config.element.field}:${index}:${fieldKey}`;
-							input.value = data[fieldKey] || '';
-						});
-
-						config.ui.items.append(newItem);
-						config.ui.inputs[0]?.focus();
-
-						this.a11y.announce('Item added');
-					}
-					removeTagListItem(tag) {
-						let tagList = tag.closest('[data-tag-list-id]');
-						tag.remove();
-						this.reindexList(tagList);
-						this.a11y.announce('Item removed');
-					}
-				handleTagListInput(e) {
-					let target = e.target;
-					let field = target.closest('[data-tag-list-id]');
-					if (!field) return;
-					let config = this.tagLists.get(field.dataset.tagListId);
-					if (!config) return;
-
-					if (e.key === 'Enter') {
-						if (target === config.ui.inputs[config.ui.inputs.length - 1]) {
-							e.preventDefault();
-							this.addTagListItem(target.closest('[data-tag-list-id]'));
-						} else {
-							e.preventDefault();
-							let index = config.ui.inputs.indexOf(target);
-							config.ui.inputs[index+1].focus();
-						}
-					}
-
-				}
-
-			checkForConditionalFields(form) {
-				form.querySelectorAll(this.selectors.dependsOn).forEach( field => {
-					const dependsOn = field.dataset.dependsOn;
-					const requiredValue = field.dataset.dependsValue;
-					const operator = field.dataset.dependsOperatior??'==';
-
-					if (!this.dependencies.has(dependsOn)) {
-						let element = document.querySelector(`[field="${dependsOn}"]`);
-						if (element) {
-							this.dependencies.set(dependsOn, {
-								element: element,
-								items: []
-							});
-						}
-					}
-					let dependency = this.dependencies.get(dependsOn);
-					dependency.items.push({
-						field: field,
-						form: form.dataset.formId,
-						requiredValue: requiredValue,
-						operator: operator
-					});
-					this.dependencies.set(dependsOn, dependency);
-					this.checkFieldDependency(dependency, dependsOn);
-				});
-			}
-				checkFieldDependency(dependentField, controlFieldName) {
-					const controlField = this.dependencies.get(controlFieldName);
-					if (!controlField) return;
-
-					const controlValue = this.getFieldValue(controlField.element);
-					const shouldShow = this.evaluateCondition(
-						controlValue,
-						dependentField.requiredValue,
-						dependentField.operator
-					);
-
-					this.toggleFieldVisibility(dependentField.field, shouldShow);
-				}
-				evaluateCondition(value, requiredValue, operator) {
-					const fieldStr = String(value || '');
-					const requiredStr = String(requiredValue || '');
-
-					switch (operator) {
-						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);
-						case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
-						case 'contains': return fieldStr.includes(requiredStr);
-						case 'empty': return fieldStr === '';
-						case 'not_empty': return fieldStr !== '';
-						default: return fieldStr === requiredStr;
-					}
-				}
-				toggleFieldVisibility(field, show) {
-					const wrapper = field.closest('.field, fieldset');
-					if (!wrapper) return;
-
-					wrapper.hidden = !show;
-					wrapper.querySelectorAll('input, select, textarea').forEach(control => {
-						control.disabled = !show;
-						if (!show && control.hasAttribute('required')) {
-							control.dataset.wasRequired = 'true';
-							control.removeAttribute('required');
-						} else if (show && control.dataset.wasRequired === 'true') {
-							control.setAttribute('required', '');
-							delete control.dataset.wasRequired;
-						}
-					});
-				}
-			checkForCharacterLimits(form) {
-				if (!form.querySelector(this.selectors.limits.hasLimit)) return;
-				this.countUpdaters = this.updateCount.bind(this);
-
-				form.querySelectorAll(`${this.selectors.limits.hasLimit}`).forEach(input => {
-					let id = window.generateID('limit');
-					input.dataset.charLimitId = id;
-					let config = {
-						element: input,
-						form: form.dataset.formId,
-						ui: window.uiFromSelectors(this.selectors.limits, input.closest('.field'))
-					};
-					config.ui.limit.textContent = input.dataset.limit;
-					this.charLimits.set(id, config);
-
-					this.addCharacterLimitListeners(input);
-				});
-
-			}
-				addCharacterLimitListeners(input) {
-					input.addEventListener('input', this.countUpdaters, {passive: true});
-				}
-				removeCharacterLimitListeners(input) {
-					input.removeEventListener('input', this.countUpdaters, {passive: true});
-				}
-				updateCount(e) {
-					let target = e.target;
-					let config = this.charLimits.get(target.dataset.charLimitId);
-					if (!config) return;
-					let length = target.value.length;
-					let limit = target.dataset.limit;
-					if (config.ui.current) {
-						config.ui.current.textContent = length;
-						config.ui.current.classList.toggle('exceeded', length >= limit);
-					}
-					if (length > limit) {
-						target.value = target.value.slice(0, limit);
-					}
-				}
-			checkForImageUploads(form, config) {
-				window.jvbUploads.scanFields(form, config.autoUpload);
-			}
-
-			checkForTabs(form, config) {
-				if (window.jvbTabs && form.querySelector('nav.tabs')) {
-					config.tabs = window.jvbTabs.registerTab(form, {
-						preCheck: (section, tabConfig) => {
-							return this.validateStep(section, config);
-						}
-					});
-					config.ui.tabs = window.uiFromSelectors(this.selectors.tabs, form);
-					config.ui.tabs.sections = Array.from(form.querySelectorAll(this.selectors.tabs.sections));
-					config.ui.tabs.inputs = {};
-					config.ui.tabs.sections.forEach(section => {
-						config.ui.tabs.inputs[section.dataset.tab] = Array.from(section.querySelectorAll(this.inputs));
-					});
-					config.ui.tabs.buttons = Array.from(form.querySelectorAll(this.selectors.tabs.buttons));
-
-					config.unsubscribeTabs = window.jvbTabs.subscribe((event, data) => {
-						if (event === 'tab-switched') {
-							if (config.ui.tabs.progress) {
-								const section = config.ui.tabs.sections.filter(section => section.dataset.tab === data.current)[0]??false;
-								if (!section) return;
-								const step = section.dataset.step;
-								const total = config.ui.sections.length;
-
-								window.showProgress(
-									config.ui.tabs.progress,
-									step,
-									total
-								);
-							}
-						}
-					});
-					this.forms.set(config.id, config);
-				}
-			}
-			validateStep(section, config) {
-				const formId = section.closest('[data-form-id]')?.dataset.formId;
-				if (!formId) return true;
-
-				const form = this.forms.get(formId);
-				if (!form) return true;
-
-				const inputs = Array.from(this.inputs.values())
-					.filter(item =>
-						item &&
-						item.form === formId &&
-						item.section === section.dataset.tab &&
-						!item.element.closest('[hidden]')
-					);
-
-				return inputs.every(item => this.validateField(item.element) === true);
-			}
-			checkForSelectors(form) {
-				if (window.jvbSelector) window.jvbSelector.scanExistingFields(form);
-			}
+		return inputs.every(item => this.validateField(item.element) === true);
+	}
+	checkForSelectors(form) {
+		if (window.jvbSelector) window.jvbSelector.scanExistingFields(form);
+	}
 	/**
 	 * Mainly for repeaters or taglist
 	 * @param {HTMLElement} container
 	 */
 	reindexList(container) {
+		const fieldName = container.dataset.field || container.dataset.repeaterId || container.dataset.tagListId;
+
 		Array.from(container.children).forEach((item, index) => {
 			item.dataset.index = `${index}`;
-			Array.from(item.children).forEach(child => {
-				if (child.type === 'hidden') {
-					window.prefixInput(
-						child,
-						`${container.dataset.field}:${index}:${child.dataset.field}`
-					);
-				}
+
+			// Find ALL inputs within this item, not just direct children
+			const inputs = item.querySelectorAll('input, select, textarea');
+
+			inputs.forEach(input => {
+				// Skip inputs that shouldn't be re-indexed (like file inputs)
+				if (input.type === 'file') return;
+
+				// Get the field name from the input's data-field or name
+				const inputField = input.dataset.field || input.name.split(':').pop();
+
+				// Re-prefix with the new index, passing item as wrapper
+				window.prefixInput(
+					input,
+					`${fieldName}:${index}:`,
+					item,
+					false,
+					true
+				);
 			});
 		});
 
-		//schedule save
+
+		this.updateCollectionField(container);
+	}
+
+	/**
+	 * Update the entire repeater/tagList field data
+	 * Call this whenever rows are added, removed, or reordered
+	 */
+	updateCollectionField(element) {
+		const field = element.closest('[data-field]');
+		if (!field) return;
+
+		const fieldType = field.dataset.fieldType;
+		if (!['repeater', 'tag-list'].includes(fieldType)) return;
+
+		const form = this.getForm(element);
+		if (!form) return;
+
+		// Get all current data for the collection
+		const value = this.getFieldValue(field);
+		this.updateItem(field.dataset.field, value, form);
 	}
 	/**********************************************************************
 	 VALIDATION
-	**********************************************************************/
+	 **********************************************************************/
 	//text, email, url, tel, date, time, datetime, number
 	//select, checkbox, radio, true_false
 	//textarea
@@ -1174,8 +1423,6 @@
 		field.classList.remove('has-success');
 		field.classList.add('has-error');
 
-		if (item.ui.success) item.ui.success.hidden = true;
-		if (item.ui.error) item.ui.error.hidden = true;
 		if (item.ui.message) {
 			item.ui.message.hidden = false;
 			item.ui.message.textContent = message;
@@ -1191,9 +1438,6 @@
 		field.classList.remove('has-error');
 		field.classList.add('has-success');
 
-		if (item.ui.success) item.ui.success.hidden = false;
-		if (item.ui.error) item.ui.error.hidden = true;
-
 		if (item.ui.message) {
 			item.ui.message.hidden = message=== '';
 			item.ui.message.textContent = message;
@@ -1269,11 +1513,6 @@
 		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) {
@@ -1292,28 +1531,20 @@
 		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');
@@ -1321,12 +1552,9 @@
 			}
 
 			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}`
@@ -1334,7 +1562,6 @@
 			window.jvbA11y.announce(announcement);
 		}
 
-		// Trigger custom event
 		form.dispatchEvent(new CustomEvent('jvb-form-error', {
 			detail: data
 		}));
@@ -1348,6 +1575,8 @@
 		if (!form || !form.options.showStatus || !form.ui?.status?.status) return;
 		if (form.status === status) return;
 
+
+		form.status = status;
 		form.ui.status.status.hidden = false;
 		form.ui.status.status.classList.toggle('loading', ['uploading', 'saving'].includes(status));
 
@@ -1356,43 +1585,46 @@
 		form.ui.status.icon.className = 'icon icon-'+this.getDefaultIcon(status);
 		setTimeout(()=> form.ui.status.status.hidden = true, (status === 'submitted') ? 3000 : 10000);
 	}
-		getDefaultMessage(status) {
-			const messages = {
-				'saving': 'Saving 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'
-			};
-			return messages[status]??status;
+	getDefaultMessage(status) {
+		const messages = {
+			'saving': 'Saving 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'
+		};
+		return messages[status]??status;
+	}
+	getDefaultIcon(status) {
+		const icons = {
+			'autosaved': 'check-circle',
+			'submitted': 'check-circle',
+			'restored': 'history',
+			'error': 'close-circle',
+			'offline': 'cloud-slash',
+			'pending': 'exclamation-mark'
 		}
-		getDefaultIcon(status) {
-			const icons = {
-				'autosaved': 'check-circle',
-				'submitted': 'check-circle',
-				'restored': 'history',
-				'error': 'close-circle',
-				'offline': 'cloud-slash',
-				'pending': 'exclamation-mark'
-			}
-			return icons[status]??'';
-		}
+		return icons[status]??'';
+	}
 
 
 	/**********************************************************************
 	 SUMMARY
-	**********************************************************************/
+	 **********************************************************************/
 	showSummary(data) {
-		this.templates.create('formSummary', data);
+		let summary = this.templates.create('formSummary', data);
+		data.config.element.after(summary);
+		window.fade(data.config.element, false);
 	}
 	/**********************************************************************
 	 UTILITY
-	**********************************************************************/
+	 **********************************************************************/
 	getForm(element) {
 		let form = element.closest('[data-form-id]');
+		if (!form) return false;
 		let id = form.dataset.formId;
 		if (!id) return false;
 		let config = this.forms.get(id);
@@ -1410,6 +1642,7 @@
 	getFieldValue(element) {
 		let type = this.getFieldType(element);
 		let conf = this.getItem(element);
+
 		let fieldName = conf.field?.dataset.field??false;
 		if (!fieldName) return false;
 
@@ -1421,78 +1654,372 @@
 				return this.getTagListValue(element, conf);
 
 			case 'group':
-				//Do we actually need anything here? I think each subfield just
-				break;
+				return null;
+			//Do we actually need anything here? I think each subfield just
 
 			case 'location':
 				return this.getLocationValue(element, conf);
 
 			case 'selector':
 			case 'upload':
+			case 'gallery':
+			case 'image':
 				return this.getHiddenInputValue(element, conf, fieldName);
 
 			case 'true-false':
-				return element.value === '1'||element.value === 'on'||element.value ==='true';
-
+			case 'toggle-text':
+				return element.checked;
+			case 'checkbox':
+				// Handle multi-checkbox (name ends with [])
+				if (element.name.endsWith('[]')) {
+					return this.getCheckboxGroupValue(element, conf);
+				}
+				return element.checked ? element.value : '';
 			default:
 				return element.value;
 		}
 	}
+
+	/**
+	 * Get all checked values for a checkbox group
+	 */
+	getCheckboxGroupValue(element, conf) {
+		if (!conf.checkboxGroup) {
+			conf.checkboxGroup = conf.field?.querySelectorAll(`input[type="checkbox"][name="${element.name}"]`);
+			this.saveItem(conf);
+		}
+
+		return Array.from(conf.checkboxGroup)
+			.filter(cb => cb.checked)
+			.map(cb => cb.value);
+	}
+	/**
+	 * Get the actual user-facing value (for validation and submission)
+	 */
+	getFieldCheckedValue(element) {
+		// Handle checkboxes and radios based on checked state
+		if (element.type === 'checkbox') {
+			const type = this.getFieldType(element);
+			if (type === 'true-false') {
+				return element.checked;
+			}
+			return element.checked ? element.value : '';
+		}
+
+		if (element.type === 'radio') {
+			const radioGroup = document.querySelectorAll(`input[name="${element.name}"]`);
+			const checked = Array.from(radioGroup).find(r => r.checked);
+			return checked ? checked.value : '';
+		}
+
+		// For everything else, use existing logic
+		return this.getFieldValue(element);
+	}
+
 	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;
 	}
-		getRepeaterValue(element, conf) {
-			if (!conf.container) {
-				conf.container = conf.field?.querySelector('.repeater-items');
-				this.saveItem(conf);
-			}
-			let value = [];
-			Array.from(conf.container.children).forEach(row => {
-				let rowData = {};
-				row.querySelectorAll('[data-field]').forEach(field => {
-					rowData[field.dataset.field] = this.getFieldValue(field);
-				});
-				value.push(rowData);
+	getRepeaterValue(element, conf) {
+		const items = element.querySelector('.repeater-items');
+		if (!items) return [];
+		let ignore = ['image_data','image-title','image-caption','image-description','image-alt-text']
+		let value = [];
+		Array.from(items.children).forEach(row => {
+			let rowData = {};
+			row.querySelectorAll('[data-field]').forEach(field => {
+				if (!ignore.includes(field.dataset.field)) {
+					const input = this.getFieldInput(field);
+					if (input) {
+						rowData[field.dataset.field] = this.getFieldValue(input);
+					}
+				}
 			});
-			return value;
+			value.push(rowData);
+		});
+		return value;
+	}
+	getFieldInput(field) {
+		// For quill fields, target the specific editor textarea
+		const quillTextarea = field.querySelector('textarea[data-editor]');
+		if (quillTextarea) return quillTextarea;
+
+		return field.querySelector(this.inputSelectors);
+	}
+	getTagListValue(element, conf) {
+		if (!conf.container) {
+			conf.container = conf.field?.querySelector('.tag-items');
+			this.saveItem(conf);
 		}
-		getTagListValue(element, conf) {
-			if (!conf.container) {
-				conf.container = conf.field?.querySelector('.tag-items');
-				this.saveItem(conf);
-			}
-			let value = [];
-			Array.from(conf.container.children).forEach(item => {
-				let inputs = item.querySelectorAll('input[type="hidden"]');
-				let fieldData = {};
-				inputs.forEach(input => {
-					fieldData[input.dataset.field] = input.value;
-				});
-				value.push(fieldData);
+		let value = [];
+		Array.from(conf.container.children).forEach(item => {
+			let inputs = item.querySelectorAll('input[type="hidden"]');
+			let fieldData = {};
+			inputs.forEach(input => {
+				fieldData[input.dataset.field] = input.value;
 			});
-			return value;
+			value.push(fieldData);
+		});
+		return value;
+	}
+	getLocationValue(element, conf) {
+		if(!conf.values){
+			conf.values = Array.from(conf.field?.querySelectorAll('[data-location-field]'));
+			this.saveItem(conf);
 		}
-		getLocationValue(element, conf) {
-			if(!conf.values){
-				conf.values = Array.from(conf.field?.querySelectorAll('[data-location-field]'));
-				this.saveItem(conf);
+		let value = {};
+		conf.values.forEach(input => {
+			value[input.dataset.locationField] = input.value;
+		});
+		return value;
+	}
+	getHiddenInputValue(element, conf, fieldName) {
+		if (element.tagName !== 'INPUT' || element.type !== 'hidden'){
+			element = element.querySelector('input[type="hidden"][name="'+fieldName+'"]');
+			if (!element) {
+				return null;
 			}
-			let value = {};
-			conf.values.forEach(input => {
-				value[input.dataset.locationField] = input.value;
-			});
-			return value;
-		}
-		getHiddenInputValue(element, conf, fieldName) {
-			if (!conf.value) {
-				conf.value = conf.field?.querySelector(`input[type=hidden][name="${fieldName}"]`);
-				this.saveItem(conf);
-			}
-			return conf.value.value;
 		}
 
+		if (conf.value === undefined || conf.value !== element.value) {
+			conf.value = element.value;
+			this.saveItem(conf);
+		}
+		return conf.value;
+	}
+
+	/**
+	 * Format field value for display in summary
+	 * @param {*} value - The field value
+	 * @param {Object} input - The input config
+	 * @returns {HTMLElement|string} - Formatted display element or string
+	 */
+	formatValueForSummary(value, input) {
+		const fieldType = this.getFieldType(input.element);
+
+		// Handle empty values
+		if (this.isEmptyValue(value)) {
+			return '';
+		}
+
+		// Handle different field types
+		switch (fieldType) {
+			case 'repeater':
+				return this.formatRepeaterForSummary(value, input);
+
+			case 'tag-list':
+				return this.formatTagListForSummary(value, input);
+
+			case 'location':
+				return this.formatLocationForSummary(value);
+
+			case 'true-false':
+				return value ? 'Yes' : 'No';
+
+			case 'checkbox':
+				// Handle multi-checkbox arrays
+				if (Array.isArray(value)) {
+					return this.formatCheckboxGroupForSummary(value, input);
+				}
+				// Single checkbox - get display label
+				return this.getDisplayLabel(input, value);
+
+			case 'selector':
+			case 'upload':
+			case 'image':	//legacy, shouldn't be needed
+			case 'gallery':	//legacy, shouldn't be needed
+							   // These might need special handling depending on your needs
+				return this.formatHiddenFieldForSummary(value, input, fieldType);
+
+			default:
+				// For radio/checkbox, get the display label
+				if (typeof value === 'string') {
+					return this.getDisplayLabel(input, value);
+				}
+				// For textarea or any multi-line text, convert line breaks
+				if (typeof value === 'string' && value.includes('\n')) {
+					return this.convertLineBreaks(value);
+				}
+				return value;
+		}
+	}
+
+	/**
+	 * Format checkbox group values with labels
+	 */
+	formatCheckboxGroupForSummary(values, input) {
+		const labels = values.map(value => this.getDisplayLabel(input, value));
+		return labels.join(', ');
+	}
+
+	/**
+	 * Convert \n line breaks to HTML
+	 */
+	convertLineBreaks(text) {
+		const container = document.createElement('span');
+		container.innerHTML = text.split('\n').join('<br>');
+		return container;
+	}
+
+	/**
+	 * Format repeater data as a list
+	 */
+	formatRepeaterForSummary(rows, input) {
+		const container = document.createElement('div');
+		container.className = 'summary-repeater';
+
+		rows.forEach((row, index) => {
+			const rowDiv = document.createElement('div');
+			rowDiv.className = 'summary-repeater-row';
+
+			const rowTitle = document.createElement('strong');
+			rowTitle.textContent = `Entry ${index + 1}:`;
+			rowDiv.appendChild(rowTitle);
+
+			const fieldsList = document.createElement('ul');
+			fieldsList.className = 'summary-repeater-fields';
+
+			for (const [fieldName, fieldValue] of Object.entries(row)) {
+				if (this.isEmptyValue(fieldValue)) continue;
+
+				const li = document.createElement('li');
+
+				// Try to find the label for this subfield
+				const subFieldElement = input.field?.querySelector(`[data-field="${fieldName}"]`);
+				const label = subFieldElement?.closest('.field')?.querySelector('label')?.textContent.replace('*', '').trim() || fieldName;
+
+				li.innerHTML = `<span class="field-label">${label}:</span> <span class="field-value">${fieldValue}</span>`;
+				fieldsList.appendChild(li);
+			}
+
+			rowDiv.appendChild(fieldsList);
+			container.appendChild(rowDiv);
+		});
+
+		return container;
+	}
+
+	/**
+	 * Format tag-list data
+	 */
+	formatTagListForSummary(tags, input) {
+		const container = document.createElement('div');
+		container.className = 'summary-taglist';
+
+		const tagsList = document.createElement('ul');
+		tagsList.className = 'summary-tags';
+
+		tags.forEach(tag => {
+			const li = document.createElement('li');
+			li.className = 'summary-tag';
+
+			// Get the primary display value (first non-empty field)
+			const displayValue = Object.values(tag).find(v => !this.isEmptyValue(v)) || '';
+
+			// If there are multiple fields, show them all
+			const fields = Object.entries(tag).filter(([k, v]) => !this.isEmptyValue(v));
+			if (fields.length > 1) {
+				li.textContent = fields.map(([k, v]) => v).join(', ');
+			} else {
+				li.textContent = displayValue;
+			}
+
+			tagsList.appendChild(li);
+		});
+
+		container.appendChild(tagsList);
+		return container;
+	}
+
+	/**
+	 * Format location data
+	 */
+	formatLocationForSummary(location) {
+		const parts = [];
+
+		if (location.street) parts.push(location.street);
+		if (location.city) parts.push(location.city);
+		if (location.province) parts.push(location.province);
+		if (location.postal_code) parts.push(location.postal_code);
+		if (location.country) parts.push(location.country);
+
+		return parts.length > 0 ? parts.join(', ') : location.address || '';
+	}
+
+	/**
+	 * Format hidden field types (upload, selector)
+	 */
+	formatHiddenFieldForSummary(value, input, fieldType) {
+		if (['upload', 'gallery', 'image'].includes(fieldType)) {
+			// Get upload preview images if available
+			const uploadField = input.field?.querySelector('[data-upload-field]');
+			if (uploadField) {
+				const previews = uploadField.querySelectorAll('.item-grid.preview img');
+				if (previews.length > 0) {
+					const container = document.createElement('div');
+					container.className = 'summary-uploads';
+					previews.forEach(img => {
+						const clone = img.cloneNode(true);
+						clone.style.maxWidth = '100px';
+						clone.style.maxHeight = '100px';
+						container.appendChild(clone);
+					});
+					return container;
+				}
+			}
+			return `${value.split(',').length} file(s) uploaded`;
+		}
+
+		if (fieldType === 'selector') {
+			// Could enhance this to show selected item names if available
+			return value;
+		}
+
+		return value;
+	}
+
+	/**
+	 * Get the display label for an input value (especially for radio/checkbox)
+	 * @param {Object} input - The input config from this.inputs
+	 * @param {*} value - The field value
+	 * @returns {string} - The display label or original value
+	 */
+	getDisplayLabel(input, value) {
+		if (!input.element) return value;
+
+		const inputType = input.element.type;
+
+		// Handle radio buttons
+		if (inputType === 'radio') {
+			const radioGroup = input.field.querySelectorAll(`input[type="radio"][name="${input.element.name}"]`);
+			const selectedRadio = Array.from(radioGroup).find(radio => radio.value === value);
+			if (selectedRadio) {
+				const label = selectedRadio.closest('label') ||
+					input.field.querySelector(`label[for="${selectedRadio.id}"]`);
+				if (label) {
+					return label.textContent.replace('*', '').trim();
+				}
+			}
+		}
+
+		// Handle checkboxes (including groups)
+		if (inputType === 'checkbox' && this.getFieldType(input.element) !== 'true-false') {
+			// Find checkbox with this value in the field
+			const checkbox = input.field.querySelector(`input[type="checkbox"][value="${value}"]`);
+			if (checkbox) {
+				const label = checkbox.closest('label') ||
+					input.field.querySelector(`label[for="${checkbox.id}"]`);
+				if (label) {
+					// Get just the span content to avoid getting nested elements
+					const span = label.querySelector('span');
+					return span ? span.textContent.trim() : label.textContent.replace('*', '').trim();
+				}
+			}
+		}
+
+		return value;
+	}
 	getItem(element, formId = null) {
 		const hasID = Object.hasOwn(element.dataset, 'ref');
 		let id = (hasID) ? element.dataset.ref : window.generateID('input');
@@ -1522,7 +2049,7 @@
 	}
 	/**********************************************************************
 	 Subscription
-	**********************************************************************/
+	 **********************************************************************/
 	subscribe(callback) {
 		this.subscribers.add(callback);
 		return () => this.subscribers.delete(callback);
@@ -1539,7 +2066,7 @@
 	}
 	/**********************************************************************
 	 Cleanup
-	**********************************************************************/
+	 **********************************************************************/
 	destroy() {
 		if (this.forms.size > 0) {
 			Array.from(this.forms.values()).forEach(form => {
@@ -1566,10 +2093,10 @@
 			});
 			this.tagLists.clear();
 		}
-		if(this.charLimits.size > 0) {
+		if (this.charLimits.size > 0) {
 			Array.from(this.charLimits.values()).forEach(charLimit => {
-				charLimit.removeEventListener('input', this.countUpdaters);
-			})
+				charLimit.element.removeEventListener('input', this.countUpdaters);
+			});
 		}
 		this.inputs.clear();
 		this.forms.clear();

--
Gitblit v1.10.0