From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh

---
 assets/js/concise/FormController.js | 2004 ++++++++++++++++++++++++++++++++++++----------------------
 1 files changed, 1,230 insertions(+), 774 deletions(-)

diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 9a3f632..e5f3868 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -19,6 +19,7 @@
 
 		this.isRestoring = false;
 		this.hasListeners = false;
+		this.hasUploads = false;
 		this.summaryTemplate = false;
 
 		this.init();
@@ -30,13 +31,27 @@
 		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';
 		this.selectors = {
 			tabs: {
 				nav: 'nav.tabs',
-				sections: '.tab.content',			//querySelectorAll
+				sections: '.tab-content',			//querySelectorAll
 				progress: {
 					progress: '.progress',
 					fill: '.progress .fill',
@@ -51,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
@@ -59,11 +79,13 @@
 				field: '.field',					//querySelectorAll
 				label: 'label',
 				success: '.success',
-				error: '.success',
+				error: '.error',
 				message: '.validation-message',
 			},
 			repeater: {
 				repeater: '.repeater',				//querySelectorAll
+				row: '.repeater-row',
+				handle: '.drag-handle',
 				header: '.repeater-row-header',
 				remove: '.remove-row',
 				add: '.add-repeater-row',
@@ -73,11 +95,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
 			},
@@ -91,7 +114,7 @@
 				input: 'input[type="number"]'
 			},
 			limits: {
-				hasLimit: '[data-limit]',
+				hasLimit: '[data-maxlength]',
 				limit: '.limit',
 				current: '.current',
 			}
@@ -108,20 +131,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',
@@ -152,41 +175,59 @@
 			}
 		});
 	}
-		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" type="button" data-form-id="${formId}">Restore</button>
-        <button class="discard" type="button" 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;
-
-				let theChanges = {['fields']: changes};
-				this.populate.populate(element, theChanges);
-				this.a11y.announce('Previous changes restored');
-
-				this.isRestoring = false;
-				notification.remove();
-			});
-
-			notification.querySelector('.discard').addEventListener('click', async () => {
-				await this.store.delete(formId);
-				this.a11y.announce('Previous changes discarded');
-				notification.remove();
-			});
-
+			}
 		}
 	initValidators() {
 		this.validators = {
@@ -317,6 +358,28 @@
 				case 'dismiss-restore':
 					form.ui.status.status.hidden = true;
 					break;
+				case 'next-step':
+					e.preventDefault();
+					form.tabs.ui.buttons.forEach((btn, index) => {
+						if (btn.dataset.tab === form.tabs.activeTab) {
+							let next = form.tabs.ui.buttons[index + 1]??false;
+							if (next) {
+								window.jvbTabs.switchTab(next.dataset.tab, window.jvbTabs.getConfig(next));
+							}
+						}
+					});
+					break;
+				case 'prev-step':
+					e.preventDefault();
+					form.tabs.ui.buttons.forEach((btn, index) => {
+						if (btn.dataset.tab === form.tabs.activeTab) {
+							let prev = form.tabs.ui.buttons[index - 1]??false;
+							if (prev) {
+								window.jvbTabs.switchTab(prev.dataset.tab, window.jvbTabs.getConfig(prev));
+							}
+						}
+					});
+					break;
 			}
 		}
 	}
@@ -325,10 +388,31 @@
 		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;
+			this.maybeUpdateCollectionDisplay(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);
 			});
 		}
@@ -347,10 +431,18 @@
 		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) return;
 
@@ -377,12 +469,12 @@
 
 		if (this.subscribers.size > 0) {
 			e.preventDefault();
-			console.log('Cancelling scheduled backup and manually backing up');
-			this.cancelBackup();
-			await this.backup();
-			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
@@ -390,9 +482,14 @@
 			} else {
 				this.notify('form-submit', {
 					config: form,
-					data: this.changes.get(form.id)?.changes??{},
+					data: this.changes.get(form.id)?.changes ?? {},
 				});
 			}
+		}
+		if (form.options.endpoint && !form.options.handled) {
+			e.preventDefault();
+			console.log('Submitting form...', this.changes.get(form.id)?.changes ?? {});
+			await this.handleServerSave(form, this.changes.get(form.id)?.changes ?? {});
 
 		}
 
@@ -409,6 +506,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,
@@ -418,12 +516,88 @@
 			});
 		}
 		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;
+		}
+
+		if (form.tabs) {
+			this.updateStepProgress(form);
+		}
+
 		this.changes.set(form.id, changes);
 		if (form.options.cache) {
 			this.scheduleBackup();
 		}
+
+		if (form.options.endpoint && !form.options.handled) {
+			this.scheduleServerSave(form, changes);
+		}
 	}
+		updateStepProgress(form) {
+			console.log('First check in updateStepProgress');
+			if (!form.tabs) return;
+			let currentStep = this.getCurrentStep(form);
+			console.log('Current step: ', currentStep);
+			if (!currentStep) return;
+
+			//The progress root will be 1 less than whatever the current step is
+			currentStep = currentStep - 1;
+
+			let section = this.getCurrentSection(form);
+			console.log('Current section: ', section);
+			if (!section) return;
+
+			let total = 0;
+			let filled = 0;
+			section.childNodes.forEach(f => {
+				console.log('Child node: ', f);
+				if (f.classList.contains('field')) {
+					total++;
+					let value = this.getFieldValue(f.querySelector(this.inputSelectors));
+					console.log('Value', value);
+					if (value.length > 0) {
+						filled++;
+					}
+				}
+			});
+			//Shouldn't happen, but simplifies things if it does
+			if (total === 0) return;
+			let current = currentStep + (filled / total);
+
+			let totalSteps = form.ui.tabs.sections.length;
+			window.showProgress(
+				form.ui.tabs.progress,
+				current,
+				totalSteps,
+				`Step ${currentStep +1} of ${totalSteps}`
+			);
+		}
+			getCurrentStep(form) {
+				let step = null;
+				form.tabs.ui.buttons.forEach(btn => {
+					if (btn.dataset.tab === form.tabs.activeTab) {
+						step = btn.dataset.step;
+					}
+				});
+				return step;
+			}
+
+			getCurrentSection(form) {
+				console.log('Getting current section: ', form.tabs.ui.sections);
+				let section = null;
+				form.tabs.ui.sections.forEach(s => {
+					if (s.dataset.tab === form.tabs.activeTab) {
+						section = s;
+					}
+				});
+				return section;
+			}
 
 	scheduleBackup() {
 		window.debouncer.schedule(
@@ -478,6 +652,89 @@
 		this.changes.delete(formId);
 	}
 
+	scheduleServerSave(form, changes){
+
+		window.debouncer.schedule(
+			`form_${form.id}_server_save`,
+			async () => {
+				await this.handleServerSave(form, changes);
+			},
+			1500
+		);
+	}
+	cancelServerSave(formId) {
+		window.debouncer.cancel(`form_${formId}_server_save`);
+	}
+	async handleServerSave(form, changes) {
+		this.cancelServerSave(form.id);
+
+		if (form.useQueue && window.jvbQueue) {
+			// changes.user = window.auth.getUser();
+			window.jvbQueue.addToQueue({
+				endpoint: form.options.endpoint,
+				data: changes,
+				headers: form.options.headers??{},
+			});
+		} else {
+			try {
+				let response = await window.auth.fetch(jvbSettings.api+form.options.endpoint, {
+					body: changes,
+					headers: form.options.headers??{},
+					method: 'POST',
+				});
+
+				if (!response.ok) {
+					let errorDetails;
+					try {
+						// Attempt to parse the server's error payload as JSON
+						errorDetails = await response.json();
+					} catch {
+						// Fallback if the error response isn't JSON (e.g., HTML error page)
+						errorDetails = { message: response.statusText || 'Unknown HTTP error' };
+					}
+					this.notify('form-error', {
+						config: form,
+						data: errorDetails
+					});
+					return;
+				}
+				let result = await response.json();
+				this.notify('form-success', {
+					config: form,
+					result: result
+				});
+			} catch (error) {
+				if (error.status) {
+					this.notify('form-error', {
+						config: form,
+						data: error
+					});
+					// This is an HTTP error thrown manually (e.g., 404, 500)
+				} else if (error instanceof SyntaxError) {
+					this.notify('form-error', {
+						config: form,
+						data: {
+							status: 'error',
+							success: false,
+							message: 'Something went wrong'
+						}
+					});
+					// This happens if response.json() fails on a successful 200 OK response
+					console.error('Data Error: Server returned invalid JSON format.');
+				} else {
+					this.notify('form-error', {
+						config: form,
+						data: {
+							status: 'network_error',
+							message: 'Network error: could not connect to the server'
+						}
+					});
+				}
+			}
+
+		}
+	}
+
 
 	/**
 	 * Register a form for handling
@@ -485,12 +742,29 @@
 	 * @param {object} options
 	 */
 	registerForm(form, options) {
+		options = {
+			autoUpload: false,
+			imageMeta: true,
+			delay: 1500,
+			useQueue: true,
+			endpoint: Object.hasOwn(form.dataset, 'save') ? form.dataset.save: '',
+			showStatus: true,
+			showSummary: false,
+			handled: Object.hasOwn(form.dataset, 'handled'),
+			cache: true,
+			ignore: [],
+			... options
+		};
+
 		//Bail if form already registered
 		if (Object.hasOwn(form.dataset, 'formId') && this.forms.has(form.dataset.formId)) return;
 
 		if (!Object.hasOwn(form.dataset, 'formId')) {
 			form.dataset.formId = window.generateID('form_');
 		}
+		if (Object.hasOwn(form.dataset, 'action')) {
+			options.headers = window.auth.getHeader(form.dataset.action);
+		}
 		const formId = form.dataset.formId;
 
 		this.addFormListeners(form);
@@ -499,703 +773,864 @@
 			element: form,
 			id: formId,
 			status: '',
-			options: {
-				autoUpload: options.autoUpload??false,
-				imageMeta: options.imageMeta??true,
-				delay: options.delay??1500,
-				endpoint: options.save??form.dataset.save??'',
-				showStatus: options.showStatus??true,
-				showSummary: options.showSummary??false,
-				cache: options.cache??true,
-				ignore: options.ignore??[]
-			},
+			options: options,
 			ui: window.uiFromSelectors(this.selectors.forms, form)
 		};
 
+		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();
 				}
 			}
+		);
+	}
 
 
-			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(config.ui.items, {
+					animation: 150,
+					draggable: this.selectors.repeater.row,
+					handle: this.selectors.repeater.handle,
+					ghostClass: 'repeater-row-ghost',
+					chosenClass: 'repeater-row-chosen',
+					dragClass: 'dragging',
+					onEnd: () => {
+						this.reindexList(config.ui.items);
+					}
+				});
+			}
+
+			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.reindexList(config.ui.items);
+		this.a11y.announce('Row added');
+	}
+	removeRepeaterRow(row) {
+		let repeater = row.closest('[data-repeater-id]');
+		row.remove();
+		let items = this.repeaters.get(repeater);
+		if (items) {
+			this.reindexList(items.ui.items);
+		}
+
+		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', ...data.config.options.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) || 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;
-							}
-
-							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');
-								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);
 					}
-				}
+				},
 			);
+			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));
+		}
+	}
+	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;
+		}
 
-		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)
+		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')) {
 
-				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);
+			config.tabs = window.jvbTabs.registerTab(form, {
+				preCheck: (activeSection, switchTo, tabConfig) => {
+					return this.validateStep(activeSection, switchTo, tabConfig);
 				}
-				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.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.inputSelectors));
+			});
+			config.ui.tabs.buttons = Array.from(form.querySelectorAll(this.selectors.tabs.buttons));
 
-					value = parseFloat(conf.input.value);
+			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 - 1;
+						const total = config.ui.tabs.sections.length;
 
-					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,
+							`Step ${step + 1} of ${total}`
+						);
 					}
 				}
-			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,
-					};
-
-					if (!config.ui.addButton) return;
-
-					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();
+			});
+			this.forms.set(config.id, config);
+		}
+	}
+	validateStep(currentSection, switchTo, config) {
+		let doit = false;
+		if (config.ui.sections) {
+			config.ui.sections.forEach((section, index) => {
+				if (section === currentSection) {
+					let prev = index -1;
+					if (prev >= 0) {
+						let prevSection = config.ui.sections[prev];
+						if (prevSection && prevSection.dataset.tab === switchTo) {
+							doit = true;
 						}
 					}
-
 				}
+			});
+		}
+		if (doit) return true;
 
-			checkForConditionalFields(form) {
-				form.querySelectorAll(this.selectors.dependsOn).forEach( field => {
-					const dependsOn = field.dataset.dependsOn;
-					const requiredValue = field.dataset.dependsValue;
-					const operator = field.dataset.dependsOperatior??'==';
+		const formId = currentSection.closest('[data-form-id]')?.dataset.formId;
+		if (!formId) return true;
 
-					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 form = this.forms.get(formId);
+		if (!form) return true;
 
-					const controlValue = this.getFieldCheckedValue(controlField.element);
-					const shouldShow = this.evaluateCondition(
-						controlValue,
-						dependentField.requiredValue,
-						dependentField.operator
-					);
+		const inputs = Array.from(this.inputs.values())
+			.filter(item =>
+				item &&
+				item.form === formId &&
+				item.section === currentSection.dataset.tab &&
+				!item.element.closest('[hidden]')
+			);
 
-					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.options.autoUpload, config.options.imageMeta);
-			}
-
-			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) {
+		let fieldName = container.dataset.field || container.dataset.repeaterId || container.dataset.tagListId;
+		if (!fieldName) {
+			let parent = container.closest('[data-field]');
+			fieldName = parent.dataset.field || parent.dataset.repeaterId || parent.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}`
-					);
+
+			let rowNum = item.querySelector('.row-number');
+			if (rowNum) {
+				rowNum.textContent = `#${index + 1}`;
+			}
+			let rowTitle = item.querySelector('.row-title');
+			let rowLabel = rowTitle ? rowTitle.dataset.label : null;
+			// 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 = Object.hasOwn(input.dataset, 'field') ? input.dataset.field : input.name.split(':').pop();
+				if (inputField === rowLabel) {
+					rowTitle.textContent = window.escapeHtml(input.value);
 				}
+
+				// Re-prefix with the new index, passing item as wrapper
+				window.prefixInput(
+					input,
+					`${fieldName}:${index}:${inputField}`,
+					item,
+					true,
+					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);
+	}
+
+	//TODO: Store these selectors in the repeater object maybe?
+	maybeUpdateCollectionDisplay(field) {
+		let parent = field.closest('[data-repeater-id],[data-tag-list-id]');
+		if (!parent) {
+			return;
+		}
+		if (parent.dataset.fieldType === 'repeater') {
+			let row = field.closest('.repeater-row');
+			if (!row) return;
+
+			let title = row.querySelector('.row-title');
+			if (!title || !title.dataset.label) return;
+			console.log(field.dataset.field.split(':').pop());
+			console.log(title.dataset.label);
+			if (field.dataset.field.split(':').pop() === title.dataset.label) {
+				title.textContent = this.getFieldValue(field.querySelector(this.inputSelectors));
+			}
+		}
+
 	}
 	/**********************************************************************
 	 VALIDATION
-	**********************************************************************/
+	 **********************************************************************/
 	//text, email, url, tel, date, time, datetime, number
 	//select, checkbox, radio, true_false
 	//textarea
@@ -1229,9 +1664,13 @@
 
 		field.classList.remove('has-success');
 		field.classList.add('has-error');
+		field.scrollIntoView({
+			behavior: "smooth",
+			block: "center",
+			inline: "nearest"
+		});
+		field.querySelector(this.inputSelectors)?.focus();
 
-		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;
@@ -1247,9 +1686,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;
@@ -1343,28 +1779,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');
@@ -1372,12 +1800,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}`
@@ -1385,7 +1810,6 @@
 			window.jvbA11y.announce(announcement);
 		}
 
-		// Trigger custom event
 		form.dispatchEvent(new CustomEvent('jvb-form-error', {
 			detail: data
 		}));
@@ -1409,35 +1833,35 @@
 		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) {
 		let summary = this.templates.create('formSummary', data);
 		data.config.element.after(summary);
@@ -1445,9 +1869,10 @@
 	}
 	/**********************************************************************
 	 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);
@@ -1465,6 +1890,7 @@
 	getFieldValue(element) {
 		let type = this.getFieldType(element);
 		let conf = this.getItem(element);
+
 		let fieldName = conf.field?.dataset.field??false;
 		if (!fieldName) return false;
 
@@ -1476,18 +1902,21 @@
 				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('[]')) {
@@ -1540,56 +1969,78 @@
 		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('input[type="hidden"]').forEach(hidden => {
+				rowData[hidden.name] = hidden.value;
 			});
-			return value;
-		}
-		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);
+
+			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);
 		}
-		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;
+		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);
 		}
-		getHiddenInputValue(element, conf, fieldName) {
-			if (!conf.value) {
-				conf.value = conf.field?.querySelector(`input[type=hidden][name="${fieldName}"]`);
-				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;
 			}
-			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
@@ -1628,7 +2079,9 @@
 
 			case 'selector':
 			case 'upload':
-				// These might need special handling depending on your needs
+			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:
@@ -1750,7 +2203,7 @@
 	 * Format hidden field types (upload, selector)
 	 */
 	formatHiddenFieldForSummary(value, input, fieldType) {
-		if (fieldType === 'upload') {
+		if (['upload', 'gallery', 'image'].includes(fieldType)) {
 			// Get upload preview images if available
 			const uploadField = input.field?.querySelector('[data-upload-field]');
 			if (uploadField) {
@@ -1848,7 +2301,7 @@
 	}
 	/**********************************************************************
 	 Subscription
-	**********************************************************************/
+	 **********************************************************************/
 	subscribe(callback) {
 		this.subscribers.add(callback);
 		return () => this.subscribers.delete(callback);
@@ -1865,7 +2318,7 @@
 	}
 	/**********************************************************************
 	 Cleanup
-	**********************************************************************/
+	 **********************************************************************/
 	destroy() {
 		if (this.forms.size > 0) {
 			Array.from(this.forms.values()).forEach(form => {
@@ -1892,10 +2345,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();
@@ -1907,6 +2360,9 @@
 	window.auth.subscribe(event => {
 		if (event === 'auth-loaded') {
 			window.jvbForm = new FormController();
+			document.querySelectorAll('form[data-auto]').forEach(form => {
+				window.jvbForm.registerForm(form, {});
+			});
 		}
 	});
 });

--
Gitblit v1.10.0