From a9b3b28d001941921aa70d37fdc87c758a163a44 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 05 Jun 2026 16:47:03 +0000
Subject: [PATCH] =Some hefty changes to FeedBlock. Transitioning to loading first page in php to save on extra requests. Got a bit to do yet, but I have to work on Northeh for a bit here.

---
 assets/js/concise/FormController.js | 3799 ++++++++++++++++++++++++++--------------------------------
 1 files changed, 1,723 insertions(+), 2,076 deletions(-)

diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index a0388e2..3ba05b4 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -1,881 +1,1193 @@
 class FormController {
-	constructor(config = {}) {
-		this.config = {
-			collectFormData: false,
-			... config
-		}
+	constructor() {
+		this.a11y = window.jvbA11y;
+		this.error = window.jvbError;
+		this.queue = window.jvbQueue;
+		this.populate = window.jvbPopulate;
+
+		this.changes = new Map();
+		this.forms = new Map();
+		this.inputs = new Map();
+		this.repeaters = new Map();
+		this.tagLists = new Map();
+		this.charLimits = new Map();
+		this.quantityFields = new Map();
+		this.quillInstances = new Map(); // formId -> Set of quill instances
+		this.dependencies = new Map();
+
+		this.subscribers = new Set();
+
 		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';
+		this.selectors = {
+			tabs: {
+				nav: 'nav.tabs',
+				sections: '.tab.content',			//querySelectorAll
+				progress: {
+					progress: '.progress',
+					fill: '.progress .fill',
+					details: '.progress .details',
+					icon: '.progress .icon'
+				},
+				buttons: 'nav.tabs button',
+			},
+			dependsOn: '[data-depends-on]',
+			forms: {
+				status: {
+					status: '.fstatus',
+					message: '.fstatus .message',
+					icon: '.fstatus .icon',
+					actions: '.fstatus .actions',
+				},
+				restore: {
+					container: '.restore-form',
+					restore: '[data-action="restore"]',
+					clear: '[data-action="clear"]',
+				}
+			},
+			inputs: this.inputSelectors,					//querySelectorAll
+			fields: {
+				field: '.field',					//querySelectorAll
+				label: 'label',
+				success: '.success',
+				error: '.error',
+				message: '.validation-message',
+			},
+			repeater: {
+				repeater: '.repeater',				//querySelectorAll
+				header: '.repeater-row-header',
+				remove: '.remove-row',
+				add: '.add-repeater-row',
+				template: 'template',
+				items: '.repeater-items',
+				inputs: this.inputSelectors					//querySelectorAll
+			},
+			tagList: {
+				tagList: '.field.tag-list',			//querySelectorAll
+				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
+			},
+			tag: {
+				label: '.tag-label'
+			},
+			number: {
+				number: '.field div.quantity',
+				increase: 'button.increase',
+				decrease: 'button.decrease',
+				input: 'input[type="number"]'
+			},
+			limits: {
+				hasLimit: '[data-maxlength]',
+				limit: '.limit',
+				current: '.current',
+			}
+		};
+	}
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+		this.blurHandler =  this.handleBlur.bind(this);
+		this.inputHandler = this.handleInput.bind(this);
+		this.submitHandler = this.handleSubmit.bind(this);
+		this.quantityClick = this.handleQuantityClick.bind(this);
+		this.repeaterClick = this.handleRepeaterClick.bind(this);
+		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);
+	}
+	initStore() {
 		const store = window.jvbStore.register(
 			'forms',
 			{
 				storeName: 'forms',
-				keyPath: 'formId',
+				keyPath: 'id',
 				indexes: [
-					{ name: 'status', keyPath: 'status' },
-					{ name: 'operationId', keyPath: 'operationId' },
+					{ name: 'src', keyPath: 'src'},
 					{ name: 'timestamp', keyPath: 'timestamp' },
 					{ name: 'formType', keyPath: 'type' }
 				],
 				TTL: 7 * 24 * 60 * 1000, //7 days
-				validateData: true,
-				delayFetch: true
 			});
 		this.store = store.forms;
 
-		this.debouncer = window.debouncer;
+		this.store.subscribe((event, data)=> {
+			if (event === 'data-ready') {
+				let stored = this.store.getFiltered();
 
-		this.ignore = [];
-
-		this.populateForm = window.jvbPopulate;
-
-		this.subscribers = new Set();
-		this.forms = new Map();
-		this.specialFields = new Map();
-		this.dependencies = new Map();
-
-		// Validation (YOU ARE GREAT!)
-		this.validators = this.initValidators();
-		this.touchedFields = new Set();
-
-		// Auto-save configuration
-		this.autoSaveDefaults = {
-			delay: 3000, // 3 seconds
-			typingDelay: 1500, // 1.5 seconds for text fields
-			enabled: true
-		};
-
-		// Repeater field management
-		this.activeRepeaters = new Map();
-		this.repeaterDelays = {
-			change: 6000,
-			typing: 3000,
-			blur: 1500,
-			add: 500,
-			remove: 800,
-			reorder: 1000
-		};
-
-		this.isTimeline = window.crudManager && window.crudManager.isTimeline;
-
-		// Bind handlers
-		this.clickHandler = this.handleClick.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
-		this.submitHandler = this.handleSubmit.bind(this);
-		this.inputHandler = this.handleInput.bind(this);
-		this.blurHandler = this.handleBlur.bind(this);
-		//Processors
-		this.processRepeaterField = this.processRepeaterField.bind(this);
-		this.processGroupField = this.processGroupField.bind(this);
-		this.processLocationField = this.processLocationField.bind(this);
-		this.processRegularField = this.processRegularField.bind(this);
-
-		this.init();
-	}
-
-	async init() {
-		this.store.subscribe(this.handleStoreEvent.bind(this));
-
-		// Set up global form handlers for standalone forms
-		this.initListeners();
-		if (window.jvbQueue) {
-			window.jvbQueue.subscribe((event, data) => {
-				if (event === 'operation-completed' && data.type === 'form') {
-					this.handleOperationComplete(data);
+				let pending = stored.filter(form=> form.src ===  window.location.pathname);
+				for (let form of pending) {
+					this.showPendingNotification(form.id, form.changes);
 				}
-			});
-		}
-	}
-
-	/**
-	 * Handle operation completion - clear related form cache
-	 */
-	async handleOperationComplete(operation) {
-		// Clear the form data from store
-		if (operation.formId) {
-			try {
-				await this.store.delete(operation.formId);
-			} catch (error) {
-				console.warn('Failed to clear form cache:', error);
+			} else if (event === 'operation-status' && data.status === 'completed') {
+				if (data.config) {
+					this.store.delete(data.config.id);
+				}
 			}
-		}
-
-		// Clear any related form state
-		const form = this.forms.get(operation.formId);
-		if (form) {
-			form.isDirty = false;
-			form.lastSaved = Date.now();
-			form.data = {};
-		}
-	}
-
-	handleStoreEvent(event, data) {
-		switch(event) {
-			case 'item-saved':
-				if (data.item.status === 'autosave') {
-					// this.showFormStatus(data.item.formId, 'autosave');
-				}
-				break;
-			case 'data-loaded':
-				this.checkPendingForms();
-				break;
-		}
-	}
-
-	/**
-	 * Check for pending forms from current page
-	 */
-	async checkPendingForms() {
-		const allForms = await this.store.getAll();
-		const currentPath = window.location.pathname;
-
-		const pendingForms = allForms.filter(form => {
-			if (form.status !== 'draft') return false;
-
-			// Check if form is from current page
-			const formPath = form.data?._wp_http_referer;
-			return formPath === currentPath;
 		});
+	}
+	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;
+		}
 
-		pendingForms.forEach(item => {
-			const formElement = this.findFormElement(item);
-			if (!formElement) return;
-
-			// Register form if not already registered
-			let formConfig = this.forms.get(item.formId);
-			if (!formElement.dataset.formId) {
-				formConfig = this.registerForm(formElement);
-			}
-
-			// Set flag to prevent event handlers from firing
+		form.ui.restore.container.hidden = false;
+		const handleRestore = async (changes, element) => {
 			this.isRestoring = true;
-			// Auto-populate the form
-			new this.populateForm(formElement, item.data);
+			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 uploads = [];
+			for (let [key, value] of Object.entries(changes)) {
+				if (key.includes('_tempUpload')) {
+					let field = key.replace('_tempUpload', '');
 
-			// Reset flag after a tick (gives DOM time to settle)
-			setTimeout(() => {
-				this.isRestoring = false;
-			}, 0);
-
-			// Show restore status
-			this.showFormStatus(item.formId, 'restored');
-
-			if (window.jvbA11y) {
-				window.jvbA11y.announce('Your previous entry has been restored');
+					if (Object.hasOwn(form.ui.uploads, field)) {
+						uploads = [
+							... uploads,
+							... value
+						];
+					}
+				}
 			}
-		});
-	}
 
-	/**
-	 * Find form element that matches the cached data
-	 */
-	findFormElement(formData) {
-		// Try by form_id first (hidden field)
-		if (formData.data?.form_id) {
-			const form = document.querySelector(`[name="form_id"][value="${formData.data.form_id}"]`)?.closest('form');
-			if (form) return form;
+			if (uploads.length > 0) {
+				if (restore) {
+					await window.jvbUploads.restoreUploads(uploads);
+				} else {
+					await window.jvbUploads.clearUploads(uploads);
+				}
+
+			}
+		}
+	initValidators() {
+		this.validators = {
+			email: {
+				pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
+				message: 'Please enter a valid email address'
+			},
+			url: {
+				pattern: /^https?:\/\/.+\..+/,
+				message: 'Please enter a valid URL starting with https://'
+			},
+			phone: {
+				pattern: /^[\d\s\-+().]+$/,
+				message: 'Please enter a valid phone number'
+			},
+			number: {
+				test: (value, fieldWrapper) => {
+					const num = parseFloat(value);
+					if (isNaN(num)) return 'Please enter a valid number';
+
+					const min = fieldWrapper.dataset.min;
+					const max = fieldWrapper.dataset.max;
+
+					if (min !== undefined && num < parseFloat(min)) {
+						return `Value must be at least ${min}`;
+					}
+					if (max !== undefined && num > parseFloat(max)) {
+						return `Value must be at most ${max}`;
+					}
+					return true;
+				}
+			},
+			text: {
+				test: (value, fieldWrapper) => {
+					const minLength = fieldWrapper.dataset.minlength;
+					const maxLength = fieldWrapper.dataset.maxlength;
+
+					if (minLength && value.length < parseInt(minLength)) {
+						return `Must be at least ${minLength} characters`;
+					}
+					if (maxLength && value.length > parseInt(maxLength)) {
+						return `Must be no more than ${maxLength} characters`;
+					}
+					return true;
+				}
+			}
+		};
+	}
+	validateField(input) {
+		const result = this.performValidation(input);
+		this.updateValidationUI(input, result);
+		return result.isValid;
+	}
+	performValidation(input) {
+		const field = input.closest('.field');
+		const value = this.getFieldCheckedValue(input);
+
+		if (!value && !input.required) {
+			return { isValid: true, message: '' };
 		}
 
-		// Try by form_type
-		if (formData.data?.form_type) {
-			const form = document.querySelector(`[name="form_type"][value="${formData.data.form_type}"]`)?.closest('form');
-			if (form) return form;
+		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' };
+			}
 		}
 
-		// Fallback: try by formId (if it was already registered)
-		return document.querySelector(`[data-form-id="${formData.formId}"]`);
+		if(input.checkValidity && !input.checkValidity()){
+			return {isValid: false, message: input.validationMessage};
+		}
+
+		if (value && Object.hasOwn(field.dataset, 'pattern')) {
+			const regex = new RegExp(field.dataset.pattern);
+			if (!regex.test(value)) {
+				return {isValid: false, message: field.dataset.validationMessage || 'Invalid format'};
+			}
+		}
+
+		if (Object.hasOwn(field.dataset, 'validate') || input.type) {
+			const validator = this.validators[field.dataset.validate||input.type];
+
+			if (validator && validator.pattern && !validator.pattern.test(value)) {
+				return {isValid: false, message: validator.message};
+			}
+
+			if (validator && validator.test) {
+				const result = validator.test(value, field);
+				if (result !== true) {
+					return {isValid: false, message: result};
+				}
+			}
+		}
+
+		return {isValid: true, message: ''};
+	}
+	updateValidationUI(input, result) {
+		if (result.isValid) {
+			this.showSuccess(input, result.message);
+		} else {
+			this.showError(input, result.message);
+		}
 	}
 
-	/**
-	 * Show notification for pending changes
-	 */
-	/**
-	 * Show notification for pending changes
-	 */
-	showPendingNotification(formId, formData) {
-		const formElement = document.querySelector(`[data-form-id="${formId}"]`);
-		if (!formElement) return;
-
-		const notification = document.createElement('div');
-		notification.className = 'pending-changes-notification';
-		notification.innerHTML = `
-        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>
-        <button class="restore-changes" data-form-id="${formId}">Restore</button>
-        <button class="discard-changes" data-form-id="${formId}">Discard</button>
-    `;
-
-		formElement.insertBefore(notification, formElement.firstChild);
-
-		// Add handlers
-		notification.querySelector('.restore-changes').addEventListener('click', async () => {
-			await this.restorePendingForm(formId, formData);
-			notification.remove();
-		});
-
-		notification.querySelector('.discard-changes').addEventListener('click', async () => {
-			await this.discardPendingForm(formId);
-			notification.remove();
-		});
-	}
-
-	/**
-	 * Restore pending form data
-	 */
-	async restorePendingForm(formId, formData) {
-		const form = document.querySelector(`[data-form-id="${formId}"]`);
+	handleClick(e) {
+		let form = this.getForm(e.target);
 		if (!form) return;
 
-		// Populate form with cached data
-		new this.populateForm(form, formData);
-
-		// Update status in store (mark as restored, not draft)
-		await this.store.save({
-			formId: formId,
-			data: formData,
-			status: 'restored',
-			timestamp: Date.now()
-		});
-
-		if (window.jvbA11y) {
-			window.jvbA11y.announce('Previous changes restored');
-		}
-	}
-
-	/**
-	 * Discard pending form data
-	 */
-	async discardPendingForm(formId) {
-		try {
-			await this.store.delete(formId);
-
-			if (window.jvbA11y) {
-				window.jvbA11y.announce('Previous changes discarded');
+		const itemAction = window.targetCheck(e, '[data-action]');
+		if (itemAction) {
+			let action = itemAction.dataset.action;
+			switch (action) {
+				case 'clear-form':
+					this.store.delete(form.id);
+					form.element.reset();
+					form.ui.status.status.hidden = true;
+					this.a11y.announce('Form cleared, starting fresh');
+					break;
+				case 'dismiss-restore':
+					form.ui.status.status.hidden = true;
+					break;
 			}
-		} catch (error) {
-			console.error('Failed to discard pending form:', error);
+		}
+	}
+
+	handleChange(e) {
+		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.forEach(item => {
+				this.checkFieldDependency(item, field.dataset.field);
+			});
+		}
+
+		let form = this.getForm(e.target);
+		this.updateItem(field.dataset.field, this.getFieldValue(e.target), form);
+	}
+
+	handleBlur(e) {
+		if (e.target.closest('[data-ignore]') || this.isRestoring) return;
+		let form = this.getForm(e.target);
+		if (!form) return;
+
+		let field = this.getField(e.target);
+		let fieldName = field.dataset.field;
+		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;
+
+		let field = this.getField(e.target);
+		if (!field) return;
+
+		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:${fieldName}`,
+			() => this.validateField(input),
+			500
+		);
+	}
+
+	async handleSubmit(e) {
+		let form = this.getForm(e.target);
+		if (!form) return;
+
+		if (this.subscribers.size > 0) {
+			e.preventDefault();
+
+			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
+				});
+			} else {
+				this.notify('form-submit', {
+					config: form,
+					data: this.changes.get(form.id)?.changes??{},
+				});
+			}
+
+		}
+
+		if (form.options.showSummary) {
+			const storedData = await this.store.get(form.id);
+			this.showSummary({config: form, changes: storedData?.changes});
 		}
 	}
 
 	/**
-	 * Setup global handlers for standalone forms
+	 * Updates the item, schedules caching if
+	 * @param name
+	 * @param value
+	 * @param form
 	 */
-	initListeners() {
-		// Only add if not already added
-		if (!this.globalHandlersAdded) {
-			document.addEventListener('click', this.clickHandler);
-			document.addEventListener('change', this.changeHandler);
-			document.addEventListener('blur', this.blurHandler, true);
-			document.addEventListener('input', this.inputHandler);
-			this.globalHandlersAdded = true;
+	updateItem(name, value, form) {
+		if (value === undefined) return;
+		if (!this.changes.has(form.id)) {
+			this.changes.set(form.id, {
+				id: form.id,
+				timestamp: Date.now(),
+				src: window.location.pathname,
+				changes: {},
+			});
+		}
+		let changes = this.changes.get(form.id);
+		//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() {
+		window.debouncer.schedule(
+			`form_changes`,
+			async () => {
+				if (this.changes.size > 0) {
+					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;
+		let changes = this.changes.get(formId);
+		if (changes.size === 0) return;
+		this.store.save(changes).then(()=>{});
+		this.changes.delete(formId);
+	}
+
+
 	/**
-	 * Register a standalone form (for front-end forms)
+	 * Register a form for handling
+	 * @param {HTMLElement} form
+	 * @param {object} options
 	 */
-	registerForm(formElement, options = {}) {
-		if (!formElement) return;
-		const formId = formElement.dataset.formId || `form_${Date.now()}`;
-		formElement.dataset.formId = formId;
+	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;
 
-		formElement.addEventListener('submit', this.submitHandler);
+		if (!Object.hasOwn(form.dataset, 'formId')) {
+			form.dataset.formId = window.generateID('form_');
+		}
+		const formId = form.dataset.formId;
 
-		const formConfig = {
-			element: formElement,
+		this.addFormListeners(form);
+
+		const config = {
+			element: form,
 			id: formId,
 			status: '',
-			options: {
-				autosave: 'autosave' in formElement.dataset,
-				autoUpload: true,
-				saveDelay: this.autoSaveDefaults.delay,
-				endpoint: formElement.dataset.save ?? '',
-				formStatus: true,
-				cache: true,
-				...options
-			},
-			dependencies: new Map(),
-			data: this.collectFormData(formElement, true),
+			options: options,
+			ui: window.uiFromSelectors(this.selectors.forms, form)
 		};
 
-		this.initializeFormFields(formElement, formConfig);
-		this.forms.set(formId, formConfig);
+		config.ui.fields = {};
+		form.querySelectorAll('[data-field]').forEach((field) => {
+			config.ui.fields[field.dataset.field] = field;
+		});
 
-		// Check for pending data - FIXED
-		if (this.store && formConfig.options.cache) {
-			const cached = this.store.get(formId);
-			if (cached && cached.data) {
-				this.showPendingNotification(formId, cached.data);
-			}
-		}
+		this.initializeFields(form, config);
+		this.forms.set(formId, config);
 
-		return formConfig;
+		return config;
 	}
+	clearForm(formId) {
+		const config = this.forms.get(formId);
+		if (!config) return;
 
-	/**
-	 * Initialize all special fields in a form
-	 */
-	initializeFormFields(form, formConfig = null) {
-		// Initialize Quill editors
-		this.initQuillEditors(form);
-
-		// Initialize repeater fields
-		this.initRepeaterFields(form, formConfig);
-
-		this.initTagListFields(form, formConfig);
-
-		// Initialize conditional fields
-		if (formConfig) {
-			this.initConditionalFields(form, formConfig);
+		if (config.unsubscribeTabs) {
+			config.unsubscribeTabs();
+		}
+		if(config.tabs) {
+			window.jvbTabs.removeTab(config.element);
 		}
 
-		// Initialize character limits
-		this.initCharacterLimits(form);
+		if (config.cache && this.changes.has(formId)) this.saveCache(formId);
 
-		// Initialize image upload fields
-		this.initImageUploadFields(form, formConfig);
-
-		// Initialize tabs if present
-		if (window.jvbTabs && form.querySelector('nav.tabs')) {
-			formConfig.tabs = new window.jvbTabs(form);
-			this.forms.set(formConfig.formId, formConfig);
-			this.initSteppedForm(formConfig.formId);
-		}
-
-		// Scan for existing selector fields
-		if (window.jvbSelector) {
-			window.jvbSelector.scanExistingFields(form);
-		}
-	}
-
-	/**
-	 * Initialize stepped form functionality
-	 */
-	initSteppedForm(formId) {
-		const formConfig = this.forms.get(formId);
-		const form = formConfig.element;
-		const tabsInstance = formConfig.tabs;
-
-		const sections = form.querySelectorAll('.tab-content');
-		const totalSteps = sections.length;
-		const progressBar = form.querySelector('.form-progress .fill');
-		const stepText = form.querySelector('.step-text .current');
-		const tabButtons = form.querySelectorAll('nav.tabs button');
-
-		// Update progress display
-		const updateProgress = (currentStep) => {
-			const progress = (currentStep / totalSteps) * 100;
-			if (progressBar) {
-				progressBar.style.width = progress + '%';
+		// Cleanup items
+		for (let [id, input] of this.inputs.entries()) {
+			if (input.form === formId) {
+				this.inputs.delete(id);
 			}
-			if (stepText) {
-				stepText.textContent = currentStep;
+		}
+		// 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);
 			}
+		});
 
-			// Update tab states
-			tabButtons.forEach((btn, idx) => {
-				const stepNum = idx + 1;
-				btn.classList.remove('current', 'completed', 'pending');
+		if (Object.hasOwn(config, 'hasQuill') && this.quillInstances.has(formId)) {
+			const instances = this.quillInstances.get(formId);
+			instances.forEach(quillInstance => {
+				// Disable the editor
+				quillInstance.disable();
 
-				if (stepNum < currentStep) {
-					btn.classList.add('completed');
-				} else if (stepNum === currentStep) {
-					btn.classList.add('current');
-				} else {
-					btn.classList.add('pending');
+				// 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();
+				}
+
+				// 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();
 				}
 			});
+
+			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;
+					}
 
-		// Next/Previous button handling
-		form.addEventListener('click', (e) => {
-			const nextBtn = e.target.closest('[data-action="next-step"]');
-			const prevBtn = e.target.closest('[data-action="prev-step"]');
-
-			if (nextBtn) {
-				e.preventDefault();
-				const currentSection = nextBtn.closest('.tab-content');
-				const currentStep = parseInt(currentSection.dataset.step);
-				const nextSection = form.querySelector(`.tab-content[data-step="${currentStep + 1}"]`);
-
-				if (nextSection && this.validateStep(currentSection)) {
-					const nextTab = nextSection.dataset.tab;
-					tabsInstance.switchTab(nextTab, true);
-					updateProgress(currentStep + 1);
-
-					// Scroll to top of form
-					form.scrollIntoView({ behavior: 'smooth', block: 'start' });
-				}
-			}
-
-			if (prevBtn) {
-				e.preventDefault();
-				const currentSection = prevBtn.closest('.tab-content');
-				const currentStep = parseInt(currentSection.dataset.step);
-				const prevSection = form.querySelector(`.tab-content[data-step="${currentStep - 1}"]`);
-
-				if (prevSection) {
-					const prevTab = prevSection.dataset.tab;
-					tabsInstance.switchTab(prevTab, true);
-					updateProgress(currentStep - 1);
-
-					// Scroll to top of form
-					form.scrollIntoView({ behavior: 'smooth', block: 'start' });
-				}
-			}
-		});
-
-		// Update progress when tabs are clicked directly
-		const originalSwitchTab = tabsInstance.switchTab.bind(tabsInstance);
-		tabsInstance.switchTab = (tab, updateHistory) => {
-			originalSwitchTab(tab, updateHistory);
-			const activeSection = form.querySelector(`.tab-content[data-tab="${tab}"]`);
-			if (activeSection) {
-				const step = parseInt(activeSection.dataset.step);
-				updateProgress(step);
-			}
-		};
-
-		// Initialize progress
-		updateProgress(1);
-	}
-
-	/**
-	 * Validate current step before allowing progression
-	 * Can be enhanced with custom validation rules
-	 */
-	validateStep(section) {
-		const fields = section.querySelectorAll('.field');
-		let allValid = true;
-
-		fields.forEach(fieldWrapper => {
-			const input = fieldWrapper.querySelector('input, textarea, select');
-			if (input && !input.closest('[hidden]')) {
-				const isValid = this.validateField(input, fieldWrapper);
-				if (!isValid) {
-					allValid = false;
-				}
-			}
-		});
-
-		return allValid;
-	}
-
-
-	/**
-	 * Initialize Quill editors
-	 */
-	initQuillEditors(form) {
-		window.jvbQuill(form);
-	}
-
-	/**
-	 * Initialize repeater fields
-	 */
-	initRepeaterFields(form, formConfig) {
-		form.querySelectorAll('.repeater').forEach(repeater => {
-			const addButton = repeater.querySelector('.add-repeater-row');
-			const container = repeater.querySelector('.repeater-items');
-			const template = repeater.querySelector('template');
-
-			if (!addButton || !template || !container) return;
-
-			// Initialize Sortable for drag-and-drop
-			if (window.Sortable) {
-				new Sortable(container, {
-					handle: '.repeater-row-header',
-					animation: 150,
-					onEnd: () => {
-						this.updateRepeaterOrder(repeater, formConfig);
+					if (check.has(item.id)) {
+						check.delete(item.id);
 					}
 				});
 			}
-
-			// Add row handler
-			addButton.addEventListener('click', () => {
-				this.addRepeaterRow(repeater, formConfig);
-			});
-
-			// Remove row handlers
-			container.addEventListener('click', (e) => {
-				if (e.target.closest('.remove-row')) {
-					this.removeRepeaterRow(e.target.closest('.repeater-row'), formConfig);
-				}
-			});
-		});
-	}
-
-	/**
-	 * Add repeater row
-	 */
-	addRepeaterRow(repeater, formConfig) {
-		const container = repeater.querySelector('.repeater-items');
-		const template = repeater.querySelector('template');
-		const index = container.children.length;
-		const fieldName = repeater.dataset.field;
-
-		// Clone template
-		const row = template.content.cloneNode(true).firstElementChild;
-		row.dataset.index = index;
-
-		// Update field names
-		row.querySelectorAll('input, select, textarea').forEach(field => {
-			const originalName = field.name;
-			field.name = `${fieldName}:${index}:${originalName}`;
-			field.id = `${fieldName}-${index}-${originalName}`;
-
-			// Update label if exists
-			const label = field.nextElementSibling;
-			if (label && label.tagName === 'LABEL') {
-				label.htmlFor = field.id;
-			}
-		});
-
-		container.appendChild(row);
-
-		if (formConfig) {
-			this.scheduleSave(formConfig, {
-				type: 'repeater',
-				action: 'add',
-				fieldName: fieldName,
-				delay: this.repeaterDelays.add
-			});
 		}
 
-		if (window.jvbA11y) {
-			window.jvbA11y.announce('Row added');
-		}
+
+		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??[]];
 
-	/**
-	 * Remove repeater row
-	 */
-	removeRepeaterRow(row, formConfig) {
-		const repeater = row.closest('.repeater');
-		const fieldName = repeater.dataset.field;
+					for (let [key, value] of Object.entries(data.changes)) {
+						if (skipFields.includes(key) || form.isEmptyValue(value)) continue;
 
-		row.remove();
+						let input = Array.from(form.inputs.values())
+							.find(temp => temp.field?.dataset.field === key);
+						if (!input) continue;
 
-		// Reindex remaining rows
-		this.updateRepeaterOrder(repeater, formConfig);
+						let entry = refs.result.cloneNode(true);
+						let title = entry.querySelector('h3');
+						let p = entry.querySelector('p');
 
-		// Schedule save
-		if (formConfig) {
-			this.scheduleSave(formConfig, {
-				type: 'repeater',
-				action: 'remove',
-				fieldName: fieldName,
-				delay: this.repeaterDelays.remove
-			});
-		}
-
-		if (window.jvbA11y) {
-			window.jvbA11y.announce('Row removed');
-		}
-	}
-
-	/**
-	 * Update repeater order after sorting
-	 */
-	updateRepeaterOrder(repeater, formConfig) {
-		const container = repeater.querySelector('.repeater-items');
-		const fieldName = repeater.dataset.field;
-
-		// Reindex all rows
-		Array.from(container.children).forEach((row, index) => {
-			row.dataset.index = index;
-
-			// Update field names
-			row.querySelectorAll('input, select, textarea').forEach(field => {
-				const parts = field.name.split(':');
-				if (parts.length === 3) {
-					const originalName = parts[2];
-					field.name = `${fieldName}:${index}:${originalName}`;
-					field.id = `${fieldName}-${index}-${originalName}`;
-
-					// Update label
-					const label = field.nextElementSibling;
-					if (label && label.tagName === 'LABEL') {
-						label.htmlFor = field.id;
-					}
-				}
-			});
-		});
-
-		// Schedule save
-		if (formConfig) {
-			this.scheduleSave(formConfig, {
-				type: 'repeater',
-				action: 'reorder',
-				fieldName: fieldName,
-				delay: this.repeaterDelays.reorder
-			});
-		}
-	}
-
-	/**
-	 * Initialize tag list fields
-	 */
-	initTagListFields(form, formConfig) {
-		form.querySelectorAll('.field.tag-list').forEach(field => {
-			const inputRow = field.querySelector('.tag-input-row');
-			const addButton = field.querySelector('.add-tag-item');
-			const tagsContainer = field.querySelector('.tag-items');
-			const template = field.querySelector('.tag-template');
-			const fieldName = field.dataset.field;
-			const tagFormat = field.dataset.tagFormat || 'first_field';
-
-			if (!inputRow || !addButton || !tagsContainer || !template) return;
-
-			// Get all input fields in the input row (excluding the button)
-			const getInputFields = () => {
-				return Array.from(inputRow.querySelectorAll('input, select, textarea'))
-					.filter(input => !input.closest('button'));
-			};
-
-			// Add tag handler
-			const addTag = () => {
-				const inputs = getInputFields();
-				const data = {};
-				let hasValue = false;
-
-				// Collect values from inputs
-				inputs.forEach(input => {
-					const fieldName = input.name.replace('new_', '');
-					const value = this.getFieldValue(input);
-
-					if (value) hasValue = true;
-					data[fieldName] = value;
-				});
-
-				if (!hasValue) {
-					if (window.jvbA11y) {
-						window.jvbA11y.announce('Please fill in at least one field', 'error');
-					}
-					inputs[0].focus();
-					return;
-				}
-
-				// Validate required fields using data-required attribute
-				const invalidField = inputs.find(input => {
-					const isRequired = ('required' in input.dataset && input.dataset.required === '1');
-					const value = this.getFieldValue(input);
-					return isRequired && !value;
-				});
-
-				if (invalidField) {
-					const fieldWrapper = invalidField.closest('.field');
-					const fieldLabel = fieldWrapper?.querySelector('label')?.textContent || 'This field';
-					this.showError(fieldWrapper, `${fieldLabel} is required.`);
-
-					invalidField.focus();
-					return;
-				}
-
-				for (let input of inputs) {
-					let wrapper = field.closest('.field');
-					if (!this.validateField(input, wrapper)){
-						input.focus();
-						return;
-					}
-				}
-
-				// Clone template and populate
-				const index = tagsContainer.children.length;
-				const newTag = template.content.cloneNode(true).firstElementChild;
-				newTag.dataset.index = index;
-
-				// Update tag label
-				const tagLabel = newTag.querySelector('.tag-label');
-				if (tagLabel) {
-					tagLabel.textContent = this.getTagDisplayText(data, tagFormat);
-				}
-
-				// Update hidden inputs
-				newTag.querySelectorAll('input[type="hidden"]').forEach(input => {
-					const fieldKey = input.dataset.field;
-					input.name = `${fieldName}:${index}:${fieldKey}`;
-					input.value = data[fieldKey] || '';
-				});
-
-				tagsContainer.appendChild(newTag);
-
-				// Clear inputs
-				inputs.forEach(input => {
-					if (input.type === 'checkbox' || input.type === 'radio') {
-						input.checked = false;
-					} else {
-						input.value = '';
-					}
-					let field = input.closest('.field');
-					this.clearValidation(field);
-				});
-
-				// Focus first input
-				if (inputs.length > 0) {
-					inputs[0].focus();
-				}
+						// 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();
 
 
-				// Schedule save
-				if (formConfig) {
-					this.scheduleSave(formConfig, {
-						type: 'tag_list',
-						action: 'add',
-						fieldName: fieldName,
-						delay: this.autoSaveDefaults.delay
-					});
-				}
+						const formattedValue = form.formatValueForSummary(value, input);
 
-				if (window.jvbA11y) {
-					window.jvbA11y.announce('Item added');
-				}
-			};
-
-			// Add button click
-			addButton.addEventListener('click', addTag);
-
-			// Enter key support on last input
-			const inputs = getInputFields();
-			if (inputs.length > 0) {
-				// Tab through inputs, Enter on last one adds the tag
-				inputs[inputs.length - 1].addEventListener('keypress', (e) => {
-					if (e.key === 'Enter') {
-						e.preventDefault();
-						addTag();
-					}
-				});
-
-				// Enter on other inputs moves to next field
-				inputs.slice(0, -1).forEach((input, i) => {
-					input.addEventListener('keypress', (e) => {
-						if (e.key === 'Enter') {
-							e.preventDefault();
-							inputs[i + 1].focus();
+						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;
 						}
-					});
-				});
-			}
 
-			// Remove tag handler
-			tagsContainer.addEventListener('click', (e) => {
-				if (e.target.closest('.remove-tag')) {
-					const tag = e.target.closest('.tag-item');
-					const tagText = tag.querySelector('.tag-label')?.textContent || 'Item';
-
-					tag.remove();
-
-					// Reindex remaining tags
-					this.reindexTagList(tagsContainer, fieldName);
-
-					// Schedule save
-					if (formConfig) {
-						this.scheduleSave(formConfig, {
-							type: 'tag_list',
-							action: 'remove',
-							fieldName: fieldName,
-							delay: this.autoSaveDefaults.delay
+						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);
+							}
 						});
 					}
 
-					if (window.jvbA11y) {
-						window.jvbA11y.announce(`${tagText} removed`);
-					}
+					refs.result?.remove();
+					data.config.element.after(el);
+					window.fade(data.config.element, false);
 				}
-			});
-		});
+			}
+		);
 	}
 
-	/**
-	 * Reindex tag list items
-	 */
-	reindexTagList(container, baseFieldName) {
-		Array.from(container.children).forEach((tag, index) => {
-			tag.dataset.index = index;
 
-			tag.querySelectorAll('input[type="hidden"]').forEach(input => {
-				const fieldKey = input.dataset.field;
-				input.name = `${baseFieldName}:${index}:${fieldKey}`;
-			});
+	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)
+		};
+
+		for (const [selector, handler] of Object.entries(fieldHandlers)) {
+			if (container.querySelector(selector)) {
+				handler();
+			}
+		}
+
+		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);
+		}
 
-	/**
-	 * Get display text for tag based on format
-	 */
-	getTagDisplayText(data, format) {
-		const values = Object.values(data).filter(v => v);
+		if (!this.quillInstances.has(config.id)) {
+			this.quillInstances.set(config.id, new Set());
+		}
 
-		if (values.length === 0) return 'New Item';
+		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));
 
-		switch (format) {
-			case 'first_field':
-				return values[0];
+		value = parseFloat(conf.ui.input.value);
 
-			case 'all_fields':
-				return values.join(', ');
-
-			default:
-				// Template format like "{name} ({email})"
-				if (format.includes('{')) {
-					let text = format;
-					for (const [key, value] of Object.entries(data)) {
-						text = text.replace(`{${key}}`, value);
-					}
-					return text;
-				}
-				// Use specific field
-				return data[format] || values[0];
+		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) {
 
-	/**
-	 * HTML escape helper
-	 */
-	escapeHtml(text) {
-		const div = document.createElement('div');
-		div.textContent = text;
-		return div.innerHTML;
+		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(
+				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: {
+						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 => {
+							let wrapper = input.closest('.tag-item');
+							window.prefixInput(input, `${data.fieldName}:${index}:`, wrapper, false, true)
+						});
+
+						if (refs.label) {
+							refs.label.textContent = data.label;
+						}
+					}
+				},
+			);
+			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);
 	}
 
-	/**
-	 * Initialize conditional fields
-	 */
-	initConditionalFields(form, formConfig) {
-		form.querySelectorAll('[data-depends-on]').forEach(field => {
+	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;
+		}
+
+		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.dependsOperator || '==';
+			const operator = field.dataset.dependsOperatior??'==';
 
-			// Store dependency
-			if (!formConfig.dependencies.has(dependsOn)) {
-				formConfig.dependencies.set(dependsOn, []);
+			let formData = this.forms.get(form.dataset.formId);
+
+			if (!this.dependencies.has(dependsOn)) {
+				if (Object.hasOwn(formData.ui.fields, dependsOn)) {
+					this.dependencies.set(dependsOn, []);
+				}
 			}
-			formConfig.dependencies.get(dependsOn).push({
-				field: field,
-				requiredValue: requiredValue,
-				operator: operator
-			});
+			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);
+			}
 
-			// Check initial state
-			this.checkFieldDependency(form, field, dependsOn, requiredValue, operator);
+			this.checkFieldDependency(field, dependsOn);
 		});
 	}
+	checkFieldDependency(dependentField, controlFieldName) {
+		const form = this.getForm(dependentField);
+		const controlField = this.dependencies.get(controlFieldName);
+		if (!controlField) return;
 
-	/**
-	 * Check field dependency
-	 */
-	checkFieldDependency(form, field, dependsOn, requiredValue, operator) {
-		const triggerField = form.querySelector(`[name="${dependsOn}"]`);
-		if (!triggerField) return;
 
-		const value = this.getFieldValue(triggerField);
-		const shouldShow = this.evaluateCondition(value, requiredValue, operator);
+		const controlValue = this.getFieldValue(form.ui.fields[controlFieldName]);
+		const shouldShow = this.evaluateCondition(
+			controlValue,
+			dependentField.dataset.dependsValue,
+			dependentField.dataset.dependsOperatior
+		);
 
-		this.toggleFieldVisibility(field, shouldShow);
+		this.toggleFieldVisibility(dependentField, shouldShow);
 	}
-
-	/**
-	 * Evaluate conditional operator
-	 */
 	evaluateCondition(value, requiredValue, operator) {
 		const fieldStr = String(value || '');
 		const requiredStr = String(requiredValue || '');
@@ -893,10 +1205,6 @@
 			default: return fieldStr === requiredStr;
 		}
 	}
-
-	/**
-	 * Toggle field visibility
-	 */
 	toggleFieldVisibility(field, show) {
 		const wrapper = field.closest('.field, fieldset');
 		if (!wrapper) return;
@@ -913,71 +1221,226 @@
 			}
 		});
 	}
+	checkForCharacterLimits(form) {
+		if (!form.querySelector(this.selectors.limits.hasLimit)) return;
+		this.countUpdaters = this.updateCount.bind(this);
 
-	/**
-	 * Initialize character limits
-	 */
-	initCharacterLimits(form) {
-		form.querySelectorAll('[data-limit]').forEach(input => {
-			const limit = parseInt(input.dataset.limit, 10);
-			const field = input.closest('.field');
+		form.querySelectorAll(this.selectors.limits.hasLimit).forEach(field => {
+			const input = this.getFieldInput(field);
+			if (!input) return;
 
-			// Create counter if it doesn't exist
-			let counter = field?.querySelector('.char-count');
-			if (!counter && field) {
-				counter = document.createElement('div');
-				counter.className = 'char-count';
-				counter.innerHTML = `<span class="current">0</span> / <span class="limit">${limit}</span>`;
-				field.appendChild(counter);
-			}
+			let id = window.generateID('limit');
+			input.dataset.charLimitId = id;
+			input.dataset.limit = field.dataset.maxlength;
 
-			const updateCount = () => {
-				const length = input.value.length;
-				if (counter) {
-					counter.querySelector('.current').textContent = length;
-					counter.classList.toggle('exceeded', length > limit);
-				}
-
-				// Truncate if exceeds limit
-				if (length > limit) {
-					input.value = input.value.substring(0, limit);
-					if (counter) {
-						counter.querySelector('.current').textContent = limit;
-					}
-				}
+			let config = {
+				element: input,
+				form: form.dataset.formId,
+				ui: window.uiFromSelectors(this.selectors.limits, field)
 			};
 
-			input.addEventListener('input', updateCount);
-			updateCount(); // Initial count
+			if (config.ui.limit) {
+				config.ui.limit.textContent = field.dataset.maxlength;
+			}
+
+			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;
+			});
+		}
+	}
+
+	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);
+	}
+	/**
+	 * 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}`;
+
+			// 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
+				);
+			});
+		});
+
+
+		this.updateCollectionField(container);
+	}
 
 	/**
-	 * Initialize image upload fields
+	 * Update the entire repeater/tagList field data
+	 * Call this whenever rows are added, removed, or reordered
 	 */
-	initImageUploadFields(form, config) {
-		window.jvbUploads.scanFields(form, config.options.autoUpload);
+	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
+	//repeater: subfields validation; no submission until all required are entered
+	//tag fields: similar to repeater; each separate field is its own hidden field
+	//upload: comma separated ints
+	//selector: comma separated ints
+	//location: hidden inputs for address, lat, lng, street, city, province, postal_code, country
+
+	clearValidation(input) {
+		let field = this.getField(input);
+		if (!field) return;
+		let item = this.getItem(input);
+		if (!item) return;
+
+		field.classList.remove('has-error', 'has-success');
+
+		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 = true;
+			item.ui.message.textContent = '';
+		}
 	}
 
-	/* ========== Event Handlers ========== */
+	showError(input, message = 'Invalid field') {
+		let field = this.getField(input);
+		if (!field) return;
+		let item = this.getItem(input);
+		if (!item) return;
 
-	async handleSubmit(event) {
-		const form = event.target;
+		field.classList.remove('has-success');
+		field.classList.add('has-error');
 
-		if (!form.dataset.formId) return;
-		const formConfig = this.forms.get(form.dataset.formId);
+		if (item.ui.message) {
+			item.ui.message.hidden = false;
+			item.ui.message.textContent = message;
+		}
+	}
 
-		// Handle subscriber-based forms
-		if (this.subscribers.size > 0) {
-			event.preventDefault();
-			const formData = this.collectFormData(form);
+	showSuccess(input, message = '') {
+		let field = this.getField(input);
+		if (!field) return;
+		let item = this.getItem(input);
+		if (!item) return;
 
-			// Notify subscribers (they'll handle actual submission)
-			this.notify('form-submit', {
-				formId: form.dataset.formId,
-				fullData: formData,
-				config: formConfig
-			});
+		field.classList.remove('has-error');
+		field.classList.add('has-success');
+
+		if (item.ui.message) {
+			item.ui.message.hidden = message=== '';
+			item.ui.message.textContent = message;
 		}
 	}
 
@@ -1050,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) {
@@ -1073,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');
@@ -1102,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}`
@@ -1115,548 +1562,30 @@
 			window.jvbA11y.announce(announcement);
 		}
 
-		// Trigger custom event
 		form.dispatchEvent(new CustomEvent('jvb-form-error', {
 			detail: data
 		}));
 	}
 
-	handleClick(e) {
-		if (window.targetCheck(e, 'div.quantity')) {
-			let container = window.targetCheck(e, 'div.quantity');
-			this.handleNumberClick(e, container.querySelector('input'));
-		} else if (window.targetCheck(e, '[data-action]')) {
-			let actionEl = window.targetCheck(e, '[data-action]');
-			let action = actionEl.dataset.action;
-			let form = actionEl.closest('form');
-
-			switch (action) {
-				case 'clear-form':
-					if (form?.dataset.formId) {
-						this.store.delete(form.dataset.formId);
-						form.reset();
-						// Hide the status message
-						form.querySelector('.fstatus').hidden = true;
-					}
-					if (window.jvbA11y) {
-						window.jvbA11y.announce('Form cleared, starting fresh');
-					}
-					break;
-
-				case 'dismiss-restore':
-					form.querySelector('.fstatus').hidden = true;
-					break;
-			}
-		}
-	}
-
-
-	handleNumberClick(e, input) {
-		let change = 0;
-
-		if (e.target.closest('.increase')) {
-			change += 1;
-		} else if (e.target.closest('.decrease')) {
-			change -=1;
-		}
-		if (change !== 0) {
-			let step = parseFloat(input.step);
-			//Allow for cents, but default to increasing by 1
-			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 = (input.value === '') ? 0 : parseFloat(input.value);
-
-			input.value = (value + (step * change));
-			this.handleNumberLimits(input);
-		}
-	}
-
-	handleNumberLimits(input) {
-		let [
-			min,
-			max,
-			increase,
-			decrease
-		] = [
-			input.min,
-			input.max,
-			input.closest('.quantity')?.querySelector('.increase'),
-			input.closest('.quantity')?.querySelector('.decrease')
-		];
-		let value = parseFloat(input.value);
-		if (value < min) {
-			input.value = min;
-			decrease.disabled = true;
-		} else if (value > max) {
-			input.value = max;
-			increase.disabled = false;
-		} else if (increase.disabled) {
-			increase.disabled = false;
-		} else if (decrease.disabled) {
-			decrease.disabled = false;
-		}
-	}
-
-	handleChange(event) {
-		if (event.target.closest('[data-ignore]') || this.isRestoring) {
-			return;
-		}
-		const target = event.target;
-		const form = target.form || target.closest('form');if (!form) return;
-
-		const formConfig = this.forms?.get(form.dataset.formId);
-		if (!formConfig) return;
-
-		if (formConfig.options.autosave || this.subscribers.size > 0) {
-			// Check conditional fields
-			const dependencies = formConfig.dependencies.get(target.name);
-			if (dependencies) {
-				dependencies.forEach(dep => {
-					this.checkFieldDependency(form, dep.field, target.name, dep.requiredValue, dep.operator);
-				});
-			}
-
-			// Schedule auto-save if enabled
-			const delay = this.getDelayForField(target);
-			this.scheduleSave(formConfig, delay);
-		}
-	}
-
-	handleBlur(e) {
-		if (e.target.closest('[data-ignore]') || this.isRestoring) {
-			return;
-		}
-		const target = e.target;
-		const form = target.form || target.closest('form');
-
-		if (!form) return;
-
-
-		const input = e.target.closest('input, textarea, select');
-		if (input) {
-			const fieldWrapper = this.findFieldWrapper(input);
-			if (fieldWrapper) {
-				// Mark as touched and validate
-				const fieldName = fieldWrapper.dataset.field;
-				if (fieldName) {
-					if (this.shouldDebounce(input)) {
-						window.debouncer.cancel(`validate_${fieldName}`);
-					}
-					this.touchedFields.add(fieldName);
-				}
-				this.validateField(input, fieldWrapper);
-			}
-			const formConfig = this.forms?.get(form.dataset.formId);
-			if (formConfig) {
-				// Shorter delay on blur
-				this.scheduleSave(formConfig, {
-					type: 'blur',
-					fieldName: target.name,
-					delay: 1500
-				});
-			}
-		}
-	}
-
-	handleInput(e) {
-		if (e.target.closest('[data-ignore]') || ! e.target.closest('form') || this.isRestoring) {
-			return;
-		}
-		const input = e.target.closest('input, textarea, select');
-		if (!input) return;
-
-		let form = input.closest('form');
-		this.showFormStatus(form.dataset.formId, 'pending');
-
-		const fieldWrapper = this.findFieldWrapper(input);
-		if (!fieldWrapper) return;
-
-		const fieldName = fieldWrapper.dataset.field;
-		if (fieldName) {
-			this.touchedFields.add(fieldName);
-		}
-
-		if (this.shouldDebounce(input)){
-			window.debouncer.schedule(
-				`validate_${fieldName}`,
-				() => this.validateField.bind(this),
-				500
-			)
-		}
-	}
-
-	/***************************************************************
-	 FORM VALIDATION
-	***************************************************************/
-	/**
-	 * Initialize validation rules
-	 */
-	initValidators() {
-		return {
-			email: {
-				pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
-				message: 'Please enter a valid email address'
-			},
-			url: {
-				pattern: /^https?:\/\/.+\..+/,
-				message: 'Please enter a valid URL starting with https://'
-			},
-			phone: {
-				pattern: /^[\d\s\-+().]+$/,
-				message: 'Please enter a valid phone number'
-			},
-			number: {
-				test: (value, fieldWrapper) => {
-					const num = parseFloat(value);
-					if (isNaN(num)) return 'Please enter a valid number';
-
-					const min = fieldWrapper.dataset.min;
-					const max = fieldWrapper.dataset.max;
-
-					if (min !== undefined && num < parseFloat(min)) {
-						return `Value must be at least ${min}`;
-					}
-					if (max !== undefined && num > parseFloat(max)) {
-						return `Value must be at most ${max}`;
-					}
-					return true;
-				}
-			},
-			text: {
-				test: (value, fieldWrapper) => {
-					const minLength = fieldWrapper.dataset.minlength;
-					const maxLength = fieldWrapper.dataset.maxlength;
-
-					if (minLength && value.length < parseInt(minLength)) {
-						return `Must be at least ${minLength} characters`;
-					}
-					if (maxLength && value.length > parseInt(maxLength)) {
-						return `Must be no more than ${maxLength} characters`;
-					}
-					return true;
-				}
-			}
-		};
-	}
-	/**
-	 * Find the field wrapper (handles both simple and complex fields)
-	 */
-	findFieldWrapper(input) {
-		// Try to find the closest .field wrapper
-		let wrapper = input.closest('.field');
-
-		// If we're in a repeater row, make sure we get the right field wrapper
-		if (!wrapper) {
-			wrapper = input.closest('[data-field]');
-		}
-
-		return wrapper;
-	}
-
-	/**
-	 * Check if input should be debounced
-	 */
-	shouldDebounce(input) {
-		const debounceTypes = ['text', 'email', 'url', 'tel', 'search'];
-		return debounceTypes.includes(input.type) || input.tagName === 'TEXTAREA';
-	}
-
-	/**
-	 * Validate a single field
-	 */
-	validateField(input, fieldWrapper) {
-		const value = this.getFieldValue(input);
-		const fieldName = fieldWrapper.dataset.field;
-
-		// Skip validation if field hasn't been touched yet (unless it's required)
-		if (!this.touchedFields.has(fieldName) && !input.required) {
-			return true;
-		}
-
-		// Skip validation if field is empty and not required
-		if (!value && !input.required) {
-			this.clearValidation(fieldWrapper);
-			return true;
-		}
-
-		// Check required
-		if (input.required && !value) {
-			this.showError(fieldWrapper, 'This field is required');
-			return false;
-		}
-
-		// Check HTML5 validity first
-		if (input.checkValidity && !input.checkValidity()) {
-			this.showError(fieldWrapper, input.validationMessage);
-			return false;
-		}
-
-		// Custom pattern validation from data attribute
-		const pattern = fieldWrapper.dataset.pattern;
-		if (pattern && value) {
-			const regex = new RegExp(pattern);
-			if (!regex.test(value)) {
-				const message = fieldWrapper.dataset.validationMessage || 'Invalid format';
-				this.showError(fieldWrapper, message);
-				return false;
-			}
-		}
-
-		// Type-specific validation
-		const validateType = fieldWrapper.dataset.validate || input.type;
-		if (validateType && this.validators[validateType]) {
-			const validator = this.validators[validateType];
-
-			if (validator.pattern && !validator.pattern.test(value)) {
-				this.showError(fieldWrapper, validator.message);
-				return false;
-			}
-
-			if (validator.test) {
-				const result = validator.test(value, fieldWrapper);
-				if (result !== true) {
-					this.showError(fieldWrapper, result);
-					return false;
-				}
-			}
-		}
-
-		// All validations passed
-		this.showSuccess(fieldWrapper);
-		this.notify('field-validated', input);
-		return true;
-	}
-
-
-
-	/**
-	 * Show success state (green checkmark)
-	 */
-	showSuccess(fieldWrapper, textMessage = '') {
-		if (!fieldWrapper) return;
-
-		// Find validation elements (they might be in field-input-wrapper or field-content)
-		const success = fieldWrapper.querySelector('.validation-icon.success');
-		const error = fieldWrapper.querySelector('.validation-icon.error');
-		const message = fieldWrapper.querySelector('.validation-message');
-		const input = fieldWrapper.querySelector('input, textarea, select');
-
-		// Remove error state
-		fieldWrapper.classList.remove('has-error');
-		input?.classList.remove('error');
-
-		// Add success state
-		fieldWrapper.classList.add('has-success');
-
-		// Show checkmark (if element exists)
-		if (success) {
-			success.hidden = false;
-		}
-		if (error) {
-			error.hidden = true;
-		}
-
-		// Hide error message
-		if (message) {
-			if (textMessage === '') {
-				message.hidden = true;
-				message.textContent = '';
-			} else {
-				message.hidden = false;
-				message.textContent = textMessage;
-			}
-		}
-	}
-
-	/**
-	 * Show error state (red message below field)
-	 */
-	showError(fieldWrapper, errorMessage) {
-		if (!fieldWrapper) return;
-
-		const success = fieldWrapper.querySelector('.validation-icon.success');
-		const error = fieldWrapper.querySelector('.validation-icon.error');
-		const message = fieldWrapper.querySelector('.validation-message');
-		const input = fieldWrapper.querySelector('input, textarea, select');
-
-		// Remove success state
-		fieldWrapper.classList.remove('has-success');
-
-		// Add error state
-		fieldWrapper.classList.add('has-error');
-		input?.classList.add('error');
-
-		// Hide checkmark (if element exists)
-		if (success) {
-			success.hidden = true;
-		}
-		//show x
-		if (error) {
-			error.hidden = false;
-		}
-
-		// Show error message
-		if (message) {
-			message.hidden = false;
-			message.textContent = errorMessage;
-		}
-	}
-
-	/**
-	 * Clear validation state
-	 */
-	clearValidation(fieldWrapper) {
-		if (!fieldWrapper) return;
-
-		const icon = fieldWrapper.querySelector('.validation-icon');
-		const message = fieldWrapper.querySelector('.validation-message');
-		const input = fieldWrapper.querySelector('input, textarea, select');
-
-		fieldWrapper.classList.remove('has-error', 'has-success');
-		input?.classList.remove('error');
-
-		if (icon) {
-			icon.hidden = true;
-		}
-
-		if (message) {
-			message.hidden = true;
-			message.textContent = '';
-		}
-	}
-
-	/* ========== Auto-save functionality ========== */
-	/**
-	 * Get appropriate delay based on field type and context
-	 */
-	getDelayForField(field) {
-		// Text fields get longer delay for typing
-		if (field.type === 'text' || field.type === 'textarea') {
-			return this.autoSaveDefaults.typingDelay;
-		}
-
-		// Checkboxes, radios, selects get shorter delay
-		if (['checkbox', 'radio', 'select-one', 'select-multiple'].includes(field.type)) {
-			return 1000;
-		}
-
-		// Default delay
-		return this.autoSaveDefaults.delay;
-	}
-	scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) {
-		if (!formConfig.options.autosave) {
-			return;
-		}
-		document.addEventListener('input', this.saveCheck, {passive: true});
-		const saveKey = `autosave_${formConfig.id}`;
-
-		this.debouncer.schedule(
-			saveKey,
-			() => this.autosave(formConfig),
-			delay
-		);
-	}
-
-	//Extend delay if user is currently typing
-	saveCheck(e) {
-		let form = e.target.closest('form[data-id]');
-		if (!form) {
-			return;
-		}
-
-		this.scheduleSave(this.forms.get(form.dataset.id));
-	}
-
-	async autosave(formConfig) {
-		const formData = this.collectFormData(formConfig.element);
-
-		this.showFormStatus(formConfig.id, 'saving');
-
-		// DataStore will now automatically:
-		// - Convert Sets/Maps to Arrays/Objects
-		// - Strip DOM references
-		// - Validate serializability
-		await this.store.save({
-			formId: formConfig.id,
-			data: formData,
-			status: 'draft',
-			timestamp: Date.now()
-		}).then(() => {
-			this.showFormStatus(formConfig.id, 'autosaved');
-		}).catch(error => {
-			console.error('Autosave failed:', error);
-			this.showFormStatus(formConfig.id, 'error', 'Failed to save changes');
-		});
-
-		// Get only changed fields
-		const changes = this.getChangedFields(formConfig.data, formData);
-		if (Object.keys(changes).length === 0) return;
-
-		// Update stored data
-		formConfig.data = formData;
-		this.forms.set(formConfig.id, formConfig);
-		document.removeEventListener('input', this.handleInput);
-
-		for (let [key, value] of Object.entries(formData)) {
-			// Complex fields need full data
-			if (typeof value === 'object') {
-				changes[key] = value;
-			}
-		}
-
-		// Notify
-		this.notify('form-autosave', {
-			formId: formConfig.id,
-			changes: changes,
-			fullData: formData,
-			config: formConfig
-		});
-	}
-
-	/**
-	 * Check if form has unsaved changes
-	 */
-	hasUnsavedChanges(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return false;
-
-		// Check if there are pending operations
-		if (formConfig.operations?.size > 0) return true;
-
-		// Check if current data differs from snapshot
-		const currentData = this.collectFormData(formConfig.element);
-		const changes = this.getChangedFields(formConfig.data, currentData);
-
-		return Object.keys(changes).length > 0;
-	}
-
-	showFormStatus(formID, status, message='') {
-		let form = this.forms.get(formID);
-		if (!form?.options.formStatus) {
-			return;
-		}
+	/**********************************************************************
+	STATUS
+	 **********************************************************************/
+	showFormStatus(formId, status, message ='') {
+		let form = this.forms.get(formId);
+		if (!form || !form.options.showStatus || !form.ui?.status?.status) return;
+		if (form.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));
 
-		const statusWrap = form.element.querySelector('.fstatus');
-		statusWrap.hidden = false;
-		const statusElement = statusWrap.querySelector('.message');
-		statusElement.textContent = '';
-		statusWrap.querySelector('.icon')?.remove();
-		statusWrap.querySelector('.actions')?.remove(); // Clear old actions
+		form.ui.status.message.textContent = message === '' ? this.getDefaultMessage(status) : message;
 
+		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.',
@@ -1667,7 +1596,9 @@
 			'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',
@@ -1676,791 +1607,507 @@
 			'offline': 'cloud-slash',
 			'pending': 'exclamation-mark'
 		}
-
-		let icon = window.getIcon(icons[status]);
-		if (icon) {
-			statusWrap.prepend(icon);
-		}
-
-		if (message === '') {
-			message = messages[status] || status;
-		}
-		statusElement.textContent = message;
-		statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status));
-
-		// Add action buttons for certain statuses
-		if (status === 'restored') {
-			const actions = document.createElement('div');
-			actions.className = 'actions';
-			actions.innerHTML = `
-            <button type="button" class="button button-small" data-action="dismiss-restore">Got it</button>
-            <button type="button" class="button button-small button-link" data-action="clear-form">Start over</button>
-        `;
-			statusWrap.appendChild(actions);
-
-			// Auto-dismiss after 10 seconds
-			setTimeout(() => statusWrap.hidden = true, 10000);
-		}
-
-		// Auto-hide success messages
-		if (status === 'submitted') {
-			setTimeout(() => statusWrap.hidden = true, 3000);
-		}
+		return icons[status]??'';
 	}
 
-	cleanupSpecialFields() {
-		this.specialFields.forEach(field => {
-			if (field.type === 'quill' && field.instance) {
-				// Remove Quill toolbar
-				const toolbar = field.instance.container.previousSibling;
-				if (toolbar?.classList.contains('ql-toolbar')) {
-					toolbar.remove();
-				}
-			}
-		});
 
-		this.uploader?.destroy();
-
-		this.specialFields.clear();
+	/**********************************************************************
+	 SUMMARY
+	 **********************************************************************/
+	showSummary(data) {
+		let summary = this.templates.create('formSummary', data);
+		data.config.element.after(summary);
+		window.fade(data.config.element, false);
 	}
-
-	/* ========== Form Data Methods ========== */
-
-	collectFormData(form) {
-		if (Object.hasOwn(form.dataset, 'timeline')) {
-			return this.collectTimeline(form);
-		}
-		//Table forms are handled separately
-		if (form.classList.contains('table') && form.tagName === 'FORM') {
-			return {};
-		}
-
-		const formData = new FormData(form);
-		let data = {};
-		const repeaterData = {};
-		const postData = {};
-
-		for (let [key, value] of formData.entries()) {
-			if (this.ignore.includes(key) || key.endsWith('_temp')) continue;
-
-			const processor = this.getFieldProcessor(key);
-			processor(key, value, data, repeaterData, postData, form);
-		}
-		if (Object.keys(postData).length !== 0) {
-			data = this.mergeRepeaterData(data, repeaterData);
-			return this.mergePostData(data, postData);
-		}
-		return this.mergeRepeaterData(data, repeaterData);
+	/**********************************************************************
+	 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);
+		if (!config) return false;
+		return config;
 	}
-
-	collectTimeline(form) {
-		let data = {};
-		let posts = {}; // Temporary object keyed by post ID
-		let postOrder = []; // Track order as encountered (preserves DOM/drag order)
-		let formData = new FormData(form);
-
-		for (const [key, value] of formData.entries()) {
-			if (this.ignore.includes(key) || key.endsWith('_temp')) {
-				continue;
-			}
-			const match = key.match(/^\[(\d+)](.+)$/);
-			if (match) {
-				// Timeline-specific field: [postId]fieldName
-				const [, postId, fieldName] = match;
-				if (!posts[postId]) {
-					posts[postId] = {
-						id: parseInt(postId),
-					};
-					postOrder.push(postId); // Track first occurrence
-				}
-				if (fieldName === 'post_thumbnail') {
-					posts[postId]['post_thumbnail'] = parseInt(form.querySelector(`[name="${key}"]`).closest('.item')?.dataset.id);
-				} else {
-					const processor = this.getFieldProcessor(fieldName);
-					processor(fieldName, value, posts[postId], {}, {}, form);
-				}
-
-			} else {
-				// Shared field (post_title, taxonomies, etc.)
-				const processor = this.getFieldProcessor(key);
-				processor(key, value, data, {}, {}, form);
-			}
-		}
-
-		// Convert to array in DOM order (matches menu_order)
-		data.timeline = postOrder.map(id => posts[id]);
-
-		delete data['form-id'];
-		delete data['sendAll'];
-		delete data['timeline_temp'];
-		delete data['']; // Empty key
-
-		return data;
+	getField(element) {
+		return element.closest('[data-field]');
 	}
-
-	getFieldProcessor(key) {
-		if (key.includes('::')) return this.processGroupField;
-		if (key.includes(':')) return this.processRepeaterField;
-		if (/\[[^\]]+]/.test(key)) return this.processLocationField;
-		return this.processRegularField;
+	getFieldType(element) {
+		let field = this.getField(element);
+		if (!field) return;
+		return field.dataset.fieldType;
 	}
+	getFieldValue(element) {
+		let type = this.getFieldType(element);
+		let conf = this.getItem(element);
 
-	mergeRepeaterData(data, repeaterData) {
-		Object.keys(repeaterData).forEach(fieldName => {
-			// Clean up empty rows and convert to array format
-			const cleanedRows = {};
-			Object.keys(repeaterData[fieldName]).forEach(index => {
-				const rowData = repeaterData[fieldName][index];
-				if (Object.keys(rowData).length > 0) {
-					cleanedRows[index] = rowData;
-				}
-			});
+		let fieldName = conf.field?.dataset.field??false;
+		if (!fieldName) return false;
 
-			// Convert to sequential array
-			data[fieldName] = Object.values(cleanedRows);
-		});
-		return data;
-	}
-
-	mergePostData(data, postData) {
-		for (let [postId, fields] of Object.entries(postData)) {
-			data[postId] = fields;
-		}
-		return data;
-	}
-
-	processRepeaterField(key, value, data, repeaterData, postData, form) {
-		let [fieldName, index, subField] = key.split(':');
-
-		const isArray = subField.endsWith('[]');
-		subField = subField.replace('[]', '');
-
-		//Ensure this repeater and row is in repeaterData
-		if (!repeaterData[fieldName]) {
-			repeaterData[fieldName] = {};
-		}
-		if (!repeaterData[fieldName][index]) {
-			repeaterData[fieldName][index] = {};
-		}
-
-		if (isArray || repeaterData[fieldName][index][subField]) {
-			// Initialize as array if not already
-			if (!repeaterData[fieldName][index][subField]) {
-				repeaterData[fieldName][index][subField] = [];
-			} else if (!Array.isArray(repeaterData[fieldName][index][subField])) {
-				repeaterData[fieldName][index][subField] = [repeaterData[fieldName][index][subField]];
-			}
-			repeaterData[fieldName][index][subField].push(value);
-		} else {
-			// Single value field
-			repeaterData[fieldName][index][subField] = value;
-		}
-	}
-	processGroupField(key, value, data, repeaterData, postData, form) {
-		const keys = key.split('::');
-		const rootGroup = keys[0];
-
-		// Initialize root group if it doesn't exist
-		if (!data[rootGroup]) {
-			data[rootGroup] = {};
-		}
-
-		// Build nested structure step by step
-		let current = data[rootGroup];
-		for (let i = 1; i < keys.length - 1; i++) {
-			const groupKey = keys[i];
-			if (!current[groupKey]) {
-				current[groupKey] = {};
-			}
-			current = current[groupKey];
-		}
-
-		// Set the final field value
-		const fieldKey = keys[keys.length - 1];
-
-		// Handle array values (checkboxes, multi-selects)
-		if (current[fieldKey] !== undefined) {
-			if (!Array.isArray(current[fieldKey])) {
-				current[fieldKey] = [current[fieldKey]];
-			}
-			current[fieldKey].push(value);
-		} else {
-			current[fieldKey] = value;
-		}
-	}
-	processLocationField(key, value, data, repeaterData, postData, form) {
-		let [fieldKey, v ] = key.split('[');
-		v = v.replace(']','');
-		if (!Object.hasOwn(data, fieldKey)) {
-			data[fieldKey] = {};
-
-			if (!Object.hasOwn(data, 'sendAll')) {
-				data['sendAll'] = [fieldKey];
-			} else if (!data['sendAll'].includes(fieldKey)) {
-				data['sendAll'].push(fieldKey);
-			}
-		}
-		data[fieldKey][v] = value;
-	}
-
-	processRegularField(key, value, data, repeaterData, postData, form) {
-		//handle array values (like checkboxes/selects)
-		key = key.replace('[]','');
-		if (data[key]) {
-			if (!Array.isArray(data[key])) {
-				data[key] = [data[key]];
-			}
-			data[key].push(value);
-		} else {
-			data[key] = value;
-		}
-	}
-
-	/**
-	 * Get field value (handles different input types)
-	 */
-	getFieldValue(input) {
-		if (!input) return '';
-
-		if (input.type === 'checkbox') {
-			return input.checked ? input.value || '1' : '';
-		} else if (input.type === 'radio') {
-			const checked = input.form?.querySelector(`[name="${input.name}"]:checked`);
-			return checked ? checked.value : '';
-		} else if (input.type === 'select-multiple') {
-			return Array.from(input.selectedOptions).map(o => o.value);
-		}
-
-		return input.value?.trim() || '';
-	}
-
-	getChangedFields(original, current) {
-		return window.getDifferences?.map(original, current) || {};
-	}
-
-	/*******************************************************
-	 Field Summary
-	*******************************************************/
-	/**
-	 * Show a comprehensive summary of form submission
-	 */
-	showSummary(formId, clear = 'form') {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return;
-
-		const form = formConfig.element || document.querySelector(`[data-form-id="${formId}"]`);
-		const summary = window.getTemplate('formSummary');
-		if (!summary) return;
-		const wrapper = summary.querySelector('.result');
-
-		// Fields to skip in summary
-		const skipFields = ['sendAll', ...this.ignore];
-
-		// Process each field in the form data
-		for (const [key, value] of Object.entries(formConfig.data)) {
-			// Skip ignored fields and empty values
-			if (skipFields.includes(key) || this.isEmptyValue(value)) {
-				continue;
-			}
-
-			// Get field info from form
-			const fieldInfo = this.getFieldInfo(form, key);
-
-			if (!fieldInfo.label) continue; // Skip if no label found
-
-			let field = wrapper.cloneNode(true);
-			let title = field.querySelector('h3');
-			let p = field.querySelector('p');
-
-			title.textContent = fieldInfo.label;
-
-
-			let formatted = this.formatFieldValue(value, fieldInfo.type, form);
-			if (this.isHtmlContent(formatted)) {
-				p.innerHTML = formatted;
-			} else {
-				p.textContent = formatted;
-			}
-
-			summary.append(field);
-		}
-		let uploads = form.querySelectorAll('[data-upload-field]');
-		if (uploads) {
-			uploads.forEach(upload => {
-				let label = upload.querySelector('h2').textContent;
-
-				let imgs = upload.querySelectorAll('.item-grid.preview img');
-				if (imgs) {
-					let field = wrapper.cloneNode(true);
-					let title = field.querySelector('h3');
-					let p = field.querySelector('p');
-					p.remove();
-
-					title.textContent = label;
-					imgs.forEach(img => {
-						img = img.cloneNode(true);
-						field.append(img);
-					});
-					summary.append(field);
-				}
-			});
-		}
-
-		// Remove template
-		wrapper.remove();
-
-		// Insert summary and hide form
-		clear = (clear !== 'form') ? form.closest(clear)??form : form;
-
-		clear.after(summary);
-		window.fade(clear, false);
-	}
-
-	/**
-	 * Check if a value is empty (null, undefined, empty string, empty array, empty object)
-	 */
-	isEmptyValue(value) {
-		if (value === null || value === undefined || value === '') {
-			return true;
-		}
-		if (Array.isArray(value) && value.length === 0) {
-			return true;
-		}
-		return typeof value === 'object' && Object.keys(value).length === 0;
-
-	}
-
-	/**
-	 * Get field information (label, type, etc.) from the form
-	 * Handles special field name patterns ([], ::, :, etc.)
-	 */
-	getFieldInfo(form, fieldName) {
-		// Try to find label by 'for' attribute (exact match)
-		let label = form.querySelector(`label[for="${fieldName}"]`);
-		let input = form.querySelector(`[name=${fieldName}]`);
-		// Try to find the input field - check multiple patterns
-		if (!input) {
-			// Try exact match first
-			input = form.querySelector(`[name="${fieldName}"]`);
-		}
-
-		if (!input) {
-			// Try with [] suffix (for checkboxes, multi-selects)
-			input = form.querySelector(`[name="${fieldName}[]"]`);
-		}
-
-		if (!input) {
-			// Try as fieldset legend (for checkbox/radio groups)
-			const fieldset = form.querySelector(`fieldset[data-field="${fieldName}"]`);
-			if (fieldset) {
-				label = fieldset.querySelector('legend');
-				input = fieldset.querySelector('input, select, textarea');
-			}
-		}
-
-		// Get label from input if not found yet
-		if (!label && input) {
-			// Try closest field wrapper first
-			const field = input.closest('.field, fieldset');
-			if (field) {
-				label = field.querySelector('label, legend, h2');
-			}
-		}
-
-		// Get field wrapper - always use base name (no special characters)
-		let fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"], fieldset[data-field="${fieldName}"]`);
-
-		// Determine field type
-		let fieldType = 'text';
-		if (fieldWrapper?.dataset.type) {
-			fieldType = fieldWrapper.dataset.type;
-		} else if (input) {
-			// Infer from input type
-			if (input.type === 'checkbox' && input.name.endsWith('[]')) {
-				fieldType = 'checkbox'; // checkbox group
-			} else if (input.type === 'checkbox') {
-				fieldType = 'true_false'; // single checkbox
-			} else if (input.tagName === 'SELECT' && input.multiple) {
-				fieldType = 'select'; // multi-select
-			} else {
-				fieldType = input.type || 'text';
-			}
-		}
-
-		return {
-			label: label?.textContent.replace('*', '').trim() || null,
-			type: fieldType,
-			wrapper: fieldWrapper,
-			input: input
-		};
-	}
-
-	/**
-	 * Check if content should be treated as HTML
-	 */
-	isHtmlContent(content) {
-		return typeof content === 'string' && (
-			content.includes('<br>') ||
-			content.includes('<p>') ||
-			content.includes('<ul>') ||
-			content.includes('<ol>') ||
-			content.includes('<a ') ||
-			content.includes('<strong>') ||
-			content.includes('<em>') ||
-			content.includes('<div')
-		);
-	}
-
-	/**
-	 * Format field value based on type
-	 */
-	formatFieldValue(value, type, form) {
 		switch (type) {
-			case 'textarea':
-			case 'wysiwyg':
-				// Handle rich text - check if it's actual HTML content from Quill
-				return this.formatTextareaValue(value, type);
-
-			case 'true_false':
-				return (value === '1' || value === 1 || value === true) ? 'Yes' : 'No';
-			case 'checkbox':
-				// Handle both single checkbox and checkbox groups
-				if (Array.isArray(value)) {
-					return this.formatArrayValue(value);
-				}
-				return (value === '1' || value === 1 || value === true) ? 'Yes' : value;
-
-			case 'select':
-				// Handle both single and multi-select
-				if (Array.isArray(value)) {
-					return this.formatArrayValue(value);
-				}
-				// Get label from select option
-				return this.getSelectLabel(value, form, type);
-			case 'date':
-			case 'datetime':
-			case 'time':
-				return window.formatDate ? window.formatDate(value) : value;
-
-			case 'radio':
-				// Get label from select option or radio label
-				return this.getSelectLabel(value, form, type);
-
 			case 'repeater':
-				return this.formatRepeaterValue(value);
+				return this.getRepeaterValue(element, conf);
+
+			case 'tag-list':
+				return this.getTagListValue(element, conf);
 
 			case 'group':
-				return this.formatGroupValue(value);
+				return null;
+			//Do we actually need anything here? I think each subfield just
 
 			case 'location':
-				return this.formatLocationValue(value);
+				return this.getLocationValue(element, conf);
 
+			case 'selector':
 			case 'upload':
-				return this.formatFileValue(value);
+			case 'gallery':
+			case 'image':
+				return this.getHiddenInputValue(element, conf, fieldName);
 
-			case 'number':
-				return this.formatNumber(value);
+			case 'true-false':
+			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;
+		}
+	}
 
-			case 'email':
-				return `<a href="mailto:${value}">${value}</a>`;
+	/**
+	 * 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);
+		}
 
-			case 'url':
-				return `<a href="${value}" target="_blank" rel="noopener">${value}</a>`;
+		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 : '';
+		}
 
-			case 'phone':
-				return `<a href="tel:${value.replace(/\D/g, '')}">${value}</a>`;
+		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) {
+		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);
+					}
+				}
+			});
+			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);
+		}
+		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);
+		});
+		return value;
+	}
+	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;
+			}
+		}
+
+		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:
-				// Handle arrays (multi-select, checkbox group)
-				if (Array.isArray(value)) {
-					return this.formatArrayValue(value);
+				// 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 repeater field value
+	 * Format checkbox group values with labels
 	 */
-	formatRepeaterValue(rows) {
-		if (!Array.isArray(rows) || rows.length === 0) {
-			return '<em>No entries</em>';
-		}
+	formatCheckboxGroupForSummary(values, input) {
+		const labels = values.map(value => this.getDisplayLabel(input, value));
+		return labels.join(', ');
+	}
 
-		let html = '<div class="repeater-summary">';
+	/**
+	 * 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) => {
-			html += `<div class="repeater-row">`;
-			html += `<strong>Entry ${index + 1}:</strong><ul>`;
-			for (const [key, value] of Object.entries(row)) {
-				if (!this.isEmptyValue(value)) {
-					const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
-					html += `<li><strong>${label}:</strong> ${value}</li>`;
-				}
+			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);
 			}
-			html += `</ul></div>`;
+
+			rowDiv.appendChild(fieldsList);
+			container.appendChild(rowDiv);
 		});
-		html += '</div>';
-		return html;
+
+		return container;
 	}
 
 	/**
-	 * Format group field value
+	 * Format tag-list data
 	 */
-	formatGroupValue(groupData) {
-		if (typeof groupData !== 'object' || Object.keys(groupData).length === 0) {
-			return '<em>No data</em>';
-		}
+	formatTagListForSummary(tags, input) {
+		const container = document.createElement('div');
+		container.className = 'summary-taglist';
 
-		let html = '<div class="group-summary"><ul>';
-		for (const [key, value] of Object.entries(groupData)) {
-			if (!this.isEmptyValue(value)) {
-				const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
-				// Handle nested groups
-				if (typeof value === 'object' && !Array.isArray(value)) {
-					html += `<li><strong>${label}:</strong> ${this.formatGroupValue(value)}</li>`;
-				} else {
-					html += `<li><strong>${label}:</strong> ${value}</li>`;
-				}
+		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;
 			}
-		}
-		html += '</ul></div>';
-		return html;
+
+			tagsList.appendChild(li);
+		});
+
+		container.appendChild(tagsList);
+		return container;
 	}
 
 	/**
-	 * Format location field value
+	 * Format location data
 	 */
-	formatLocationValue(location) {
-		if (typeof location !== 'object') return location;
-
+	formatLocationForSummary(location) {
 		const parts = [];
-		const fields = ['address', 'city', 'state', 'zip', 'country'];
 
-		fields.forEach(field => {
-			if (location[field]) {
-				parts.push(location[field]);
-			}
-		});
+		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.join(', ');
+		return parts.length > 0 ? parts.join(', ') : location.address || '';
 	}
 
 	/**
-	 * Format file/image value
+	 * Format hidden field types (upload, selector)
 	 */
-	formatFileValue(value) {
-		if (typeof value === 'string') {
-			// Single file - could be URL or filename
-			if (value.startsWith('http')) {
-				return `<a href="${value}" target="_blank">View file</a>`;
+	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;
 		}
 
-		if (Array.isArray(value)) {
-			return value.map(file => {
-				if (typeof file === 'string') {
-					return `<a href="${file}" target="_blank">View file</a>`;
-				}
-				return file.name || 'File';
-			}).join(', ');
-		}
-
-		return 'File uploaded';
-	}
-
-	/**
-	 * Format number with proper locale formatting
-	 */
-	formatNumber(value) {
-		const num = parseFloat(value);
-		if (isNaN(num)) return value;
-
-		// Check if it's likely currency (has 2 decimal places)
-		if (value.toString().includes('.') && value.toString().split('.')[1].length === 2) {
-			return new Intl.NumberFormat('en-CA', {
-				style: 'currency',
-				currency: 'USD'
-			}).format(num);
-		}
-
-		return new Intl.NumberFormat('en-CA').format(num);
-	}
-
-	/**
-	 * Format array values (checkboxes, multi-select)
-	 */
-	/**
-	 * Format array values (checkboxes, multi-select)
-	 */
-	formatArrayValue(arr, form = null, fieldInfo = null) {
-		if (arr.length === 0) return '<em>None selected</em>';
-
-		// If we have field info, try to get proper labels
-		if (form && fieldInfo && fieldInfo.input) {
-			const labeled = arr.map(val => {
-				return this.getSelectLabel(val, form, fieldInfo.type);
-			});
-			return '<ul><li>' + labeled.join('</li><li>') + '</li></ul>';
-		}
-
-		// Fallback to raw values
-		return '<ul><li>' + arr.join('</li><li>') + '</li></ul>';
-	}
-
-	/**
-	 * Get label for select/radio option
-	 */
-	/**
-	 * Get label for select/radio/checkbox option
-	 */
-	getSelectLabel(value, form, type) {
-		if (type === 'select') {
-			const option = form.querySelector(`option[value="${value}"]`);
-			return option?.textContent || value;
-		}
-
-		if (type === 'radio') {
-			const radio = form.querySelector(`input[type="radio"][value="${value}"]`);
-			const label = radio?.nextElementSibling;
-			return label?.textContent || value;
-		}
-
-		if (type === 'checkbox') {
-			// Try to find the checkbox with this value
-			const checkbox = form.querySelector(`input[type="checkbox"][value="${value}"]`);
-			if (checkbox) {
-				// Look for associated label
-				const label = form.querySelector(`label[for="${checkbox.id}"]`);
-				if (label) {
-					return label.textContent.trim();
-				}
-				// Try next sibling
-				const nextLabel = checkbox.nextElementSibling;
-				if (nextLabel?.tagName === 'LABEL') {
-					return nextLabel.textContent.trim();
-				}
-			}
-		}
-
 		return value;
 	}
 
 	/**
-	 * Format textarea value - handles both rich text and plain text
+	 * 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
 	 */
-	formatTextareaValue(value, type) {
-		if (!value) return '<em>Empty</em>';
+	getDisplayLabel(input, value) {
+		if (!input.element) return value;
 
-		// If it's explicitly a wysiwyg type or contains HTML tags, use as-is
-		if (type === 'wysiwyg' || this.containsHtml(value)) {
-			// Quill content already has proper HTML structure
-			return value;
+		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();
+				}
+			}
 		}
 
-		// Plain textarea - preserve formatting
-		return this.formatPlainText(value);
-	}
-
-	/**
-	 * Check if string contains HTML content (more reliable than just checking for '<')
-	 */
-	containsHtml(str) {
-		// Check for common HTML tags that Quill uses
-		const htmlPattern = /<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i;
-		return htmlPattern.test(str);
-	}
-
-	/**
-	 * Format plain text content - preserves whitespace and converts newlines
-	 */
-	formatPlainText(text) {
-		if (!text) return '';
-
-		// First, escape any HTML entities that might be in the text
-		text = text
-			.replace(/&/g, '&amp;')
-			.replace(/</g, '&lt;')
-			.replace(/>/g, '&gt;');
-
-		// Convert double newlines to paragraphs for better readability
-		const paragraphs = text.split(/\n\n+/);
-
-		if (paragraphs.length > 1) {
-			// Multiple paragraphs
-			return paragraphs
-				.map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`)
-				.join('');
+		// 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();
+				}
+			}
 		}
 
-		// Single paragraph - just convert newlines to breaks
-		return text.replace(/\n/g, '<br>');
+		return value;
 	}
+	getItem(element, formId = null) {
+		const hasID = Object.hasOwn(element.dataset, 'ref');
+		let id = (hasID) ? element.dataset.ref : window.generateID('input');
+		if (!hasID) element.dataset.ref = id;
 
-	/**
-	 * Event system
-	 */
+		//check if we have it already
+		if (!this.inputs.has(id)) {
+			if (!formId) {
+				formId = element.closest('[data-form-id]')?.dataset.formId??false;
+			}
+			let field = this.getField(element);
+
+			this.inputs.set(id, {
+				id: id,
+				element: element,
+				form: formId,
+				field: field,
+				section: element.closest('[data-tab]')?.dataset.tab ?? false,
+				ui: window.uiFromSelectors(this.selectors.fields, field)
+			});
+		}
+
+		return this.inputs.get(id);
+	}
+	saveItem(config) {
+		this.inputs.set(config.id, config);
+	}
+	/**********************************************************************
+	 Subscription
+	 **********************************************************************/
 	subscribe(callback) {
 		this.subscribers.add(callback);
 		return () => this.subscribers.delete(callback);
 	}
 
 	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
-	}
-
-	/**
-	 * Cleanup when form is closed/destroyed
-	 */
-	cleanupForm(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return;
-
-		// Check for unsaved changes
-		if (this.hasUnsavedChanges(formId)) {
-			this.autosave(formConfig);
-		}
-
-		// Clean up special fields
-		this.cleanupSpecialFields();
-
-		// Remove form config
-		this.forms.delete(formId);
-	}
-
-	/**
-	 * Cleanup
-	 */
-	destroy() {
-		// Remove global handlers
-		if (this.globalHandlersAdded) {
-			document.removeEventListener('change', this.changeHandler);
-			document.removeEventListener('blur', this.blurHandler, true);
-			document.removeEventListener('input', this.inputHandler, true);
-		}
-		this.forms.forEach((formConfig) => {
-			let element = formConfig.element;
-			if (element) {
-				element.removeEventListener('submit', this.submitHandler);
+		this.subscribers.forEach(cb => {
+			try {
+				cb(event, data);
+			} catch (e) {
+				console.error('HandleSelection subscriber error:', e);
 			}
 		});
-
-		// Clear maps
-		this.specialFields.clear();
-		this.forms.clear();
-		this.activeRepeaters.clear();
-
-		if (this.forms) {
+	}
+	/**********************************************************************
+	 Cleanup
+	 **********************************************************************/
+	destroy() {
+		if (this.forms.size > 0) {
+			Array.from(this.forms.values()).forEach(form => {
+				this.removeFormListeners(form);
+			});
 			this.forms.clear();
 		}
+		if (this.repeaters.size > 0) {
+			Array.from(this.repeaters.values()).forEach(repeater => {
+				this.removeRepeaterListeners(repeater.element);
+				repeater.sortable?.destroy();
+			});
+			this.repeaters.clear();
+		}
+		if (this.quantityFields.size > 0) {
+			Array.from(this.quantityFields.values()).forEach(num => {
+				this.removeQuantityListeners(num.element);
+			});
+			this.quantityFields.clear();
+		}
+		if (this.tagLists.size > 0) {
+			Array.from(this.tagLists.values()).forEach(tagList => {
+				this.removeTagListListeners(tagList.element);
+			});
+			this.tagLists.clear();
+		}
+		if (this.charLimits.size > 0) {
+			Array.from(this.charLimits.values()).forEach(charLimit => {
+				charLimit.element.removeEventListener('input', this.countUpdaters);
+			});
+		}
+		this.inputs.clear();
+		this.forms.clear();
+		this.charLimits.clear();
+
 	}
 }
-
 document.addEventListener('DOMContentLoaded', async function () {
 	window.auth.subscribe(event => {
 		if (event === 'auth-loaded') {
-			window.jvbForm = FormController;
+			window.jvbForm = new FormController();
 		}
 	});
-
 });

--
Gitblit v1.10.0