From 235ce5716edc2f7cbe80fdccf26eac7269587839 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 08 Jun 2026 04:38:18 +0000
Subject: [PATCH] =FavouritesManager.php and FavouritesRoutes.php fixes. Moving all logic to FavouritesManager.php. Still some left to do

---
 assets/js/concise/FormController.js | 1644 ++++++++++++++++++++++++++++++----------------------------
 1 files changed, 856 insertions(+), 788 deletions(-)

diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 70e5b6a..3ba05b4 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -19,6 +19,7 @@
 
 		this.isRestoring = false;
 		this.hasListeners = false;
+		this.hasUploads = false;
 		this.summaryTemplate = false;
 
 		this.init();
@@ -30,6 +31,20 @@
 		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';
@@ -51,7 +66,12 @@
 					status: '.fstatus',
 					message: '.fstatus .message',
 					icon: '.fstatus .icon',
-					actions: '.fstatus .actions'
+					actions: '.fstatus .actions',
+				},
+				restore: {
+					container: '.restore-form',
+					restore: '[data-action="restore"]',
+					clear: '[data-action="clear"]',
 				}
 			},
 			inputs: this.inputSelectors,					//querySelectorAll
@@ -109,20 +129,20 @@
 		this.tagListClick = this.handleTagListClick.bind(this);
 		this.tagListInput = this.handleTagListInput.bind(this);
 	}
-		addFormListeners(form) {
-			form.addEventListener('click', this.clickHandler);
-			form.addEventListener('change', this.changeHandler);
-			form.addEventListener('input', this.inputHandler);
-			form.addEventListener('blur', this.blurHandler);
-			form.addEventListener('submit', this.submitHandler);
-		}
-		removeFormListeners(form) {
-			form.removeEventListener('click', this.clickHandler);
-			form.removeEventListener('change', this.changeHandler);
-			form.removeEventListener('input', this.inputHandler);
-			form.removeEventListener('blur', this.blurHandler);
-			form.removeEventListener('submit', this.submitHandler);
-		}
+	addFormListeners(form) {
+		form.addEventListener('click', this.clickHandler);
+		form.addEventListener('change', this.changeHandler);
+		form.addEventListener('input', this.inputHandler);
+		form.addEventListener('blur', this.blurHandler);
+		form.addEventListener('submit', this.submitHandler);
+	}
+	removeFormListeners(form) {
+		form.removeEventListener('click', this.clickHandler);
+		form.removeEventListener('change', this.changeHandler);
+		form.removeEventListener('input', this.inputHandler);
+		form.removeEventListener('blur', this.blurHandler);
+		form.removeEventListener('submit', this.submitHandler);
+	}
 	initStore() {
 		const store = window.jvbStore.register(
 			'forms',
@@ -153,41 +173,59 @@
 			}
 		});
 	}
-		showPendingNotification(formId, changes) {
-			let form = this.forms.get(formId);
+	showPendingNotification(formId, changes) {
+		let form = this.forms.get(formId);
+		if (!form) return;
+		let element = form.element;
+		if (!element) {
+			console.warn(`Form element not found for: ${formId}`);
+			return;
+		}
+
+		form.ui.restore.container.hidden = false;
+		const handleRestore = async (changes, element) => {
+			this.isRestoring = true;
+			let theChanges = {['fields']: changes};
+			await this.checkStoredUploads(changes, element);
+			this.populate.populate(element, theChanges);
+			this.a11y.announce('Previous changes restored');
+			this.isRestoring = false;
+			form.ui.restore.container.remove();
+		};
+		const clearRestore = async (formId) => {
+			await this.checkStoredUploads(changes, element, false);
+			await this.store.delete(formId);
+			this.a11y.announce('Previous changes discarded');
+			form.ui.restore.container.remove();
+		};
+		form.ui.restore.restore.addEventListener('click', () => handleRestore(changes, element));
+		form.ui.restore.clear.addEventListener('click', async ()  => clearRestore(formId));
+	}
+		async checkStoredUploads(changes, element, restore = true) {
+			let form = this.forms.get(element.dataset.formId);
 			if (!form) return;
-			let element = form.element;
-			if (!element) {
-				console.warn(`Form element not found for: ${formId}`);
-				return;
+			let uploads = [];
+			for (let [key, value] of Object.entries(changes)) {
+				if (key.includes('_tempUpload')) {
+					let field = key.replace('_tempUpload', '');
+
+					if (Object.hasOwn(form.ui.uploads, field)) {
+						uploads = [
+							... uploads,
+							... value
+						];
+					}
+				}
 			}
 
-			const notification = document.createElement('div');
-			notification.className = 'pendingChanges';
-			notification.innerHTML = `
-			<p>We noticed unsaved changes from last time. Would you like to restore them?</p>
-        <button class="restore" type="button" data-form-id="${formId}">Restore</button>
-        <button class="discard" type="button" data-form-id="${formId}">Discard</button>`;
+			if (uploads.length > 0) {
+				if (restore) {
+					await window.jvbUploads.restoreUploads(uploads);
+				} else {
+					await window.jvbUploads.clearUploads(uploads);
+				}
 
-			element.insertBefore(notification, form.ui.status.status);
-
-			notification.querySelector('.restore').addEventListener('click', async () => {
-				this.isRestoring = true;
-
-				let theChanges = {['fields']: changes};
-				this.populate.populate(element, theChanges);
-				this.a11y.announce('Previous changes restored');
-
-				this.isRestoring = false;
-				notification.remove();
-			});
-
-			notification.querySelector('.discard').addEventListener('click', async () => {
-				await this.store.delete(formId);
-				this.a11y.announce('Previous changes discarded');
-				notification.remove();
-			});
-
+			}
 		}
 	initValidators() {
 		this.validators = {
@@ -333,7 +371,7 @@
 			// Dependencies still need checking
 			if (this.dependencies.has(field.dataset.field)) {
 				let dependency = this.dependencies.get(field.dataset.field);
-				dependency.items.forEach(item => {
+				dependency.forEach(item => {
 					this.checkFieldDependency(item, field.dataset.field);
 				});
 			}
@@ -349,7 +387,7 @@
 		//Dependencies
 		if (this.dependencies.has(field.dataset.field)) {
 			let dependency = this.dependencies.get(field.dataset.field);
-			dependency.items.forEach(item => {
+			dependency.forEach(item => {
 				this.checkFieldDependency(item, field.dataset.field);
 			});
 		}
@@ -438,6 +476,7 @@
 	 * @param form
 	 */
 	updateItem(name, value, form) {
+		if (value === undefined) return;
 		if (!this.changes.has(form.id)) {
 			this.changes.set(form.id, {
 				id: form.id,
@@ -447,7 +486,16 @@
 			});
 		}
 		let changes = this.changes.get(form.id);
-		changes.changes[name] = value;
+		//If it is temporary uploads, we need to store them all
+		if (name.includes('_tempUpload')) {
+			if (!Object.hasOwn(changes.changes, name)) {
+				changes.changes[name] = [];
+			}
+			changes.changes[name].push(value);
+		} else {
+			changes.changes[name] = value;
+		}
+
 		this.changes.set(form.id, changes);
 		if (form.options.cache) {
 			this.scheduleBackup();
@@ -514,6 +562,17 @@
 	 * @param {object} options
 	 */
 	registerForm(form, options) {
+		options = {
+			autoUpload: false,
+			imageMeta: true,
+			delay: 1500,
+			endpoint: Object.hasOwn(form.dataset, 'save') ? form.dataset.save: '',
+			showStatus: true,
+			showSummary: false,
+			cache: true,
+			ignore: [],
+			... options
+		};
 		//Bail if form already registered
 		if (Object.hasOwn(form.dataset, 'formId') && this.forms.has(form.dataset.formId)) return;
 
@@ -528,641 +587,640 @@
 			element: form,
 			id: formId,
 			status: '',
-			options: {
-				autoUpload: options.autoUpload??false,
-				imageMeta: options.imageMeta??true,
-				delay: options.delay??1500,
-				endpoint: options.save??form.dataset.save??'',
-				showStatus: options.showStatus??true,
-				showSummary: options.showSummary??false,
-				cache: options.cache??true,
-				ignore: options.ignore??[]
-			},
+			options: options,
 			ui: window.uiFromSelectors(this.selectors.forms, form)
 		};
 
+		config.ui.fields = {};
+		form.querySelectorAll('[data-field]').forEach((field) => {
+			config.ui.fields[field.dataset.field] = field;
+		});
+
 		this.initializeFields(form, config);
 		this.forms.set(formId, config);
 
 		return config;
 	}
-		clearForm(formId) {
-			const config = this.forms.get(formId);
-			if (!config) return;
+	clearForm(formId) {
+		const config = this.forms.get(formId);
+		if (!config) return;
 
-			if (config.unsubscribeTabs) {
-				config.unsubscribeTabs();
+		if (config.unsubscribeTabs) {
+			config.unsubscribeTabs();
+		}
+		if(config.tabs) {
+			window.jvbTabs.removeTab(config.element);
+		}
+
+		if (config.cache && this.changes.has(formId)) this.saveCache(formId);
+
+		// Cleanup items
+		for (let [id, input] of this.inputs.entries()) {
+			if (input.form === formId) {
+				this.inputs.delete(id);
 			}
-			if(config.tabs) {
-				window.jvbTabs.removeTab(config.element);
+		}
+		// Clean up dependencies for this form
+		this.dependencies.forEach((dependency, fieldName) => {
+			dependency = dependency.filter(item => item.form !== formId);
+
+			// Remove the dependency entry entirely if no items left
+			if (dependency.length === 0) {
+				this.dependencies.delete(fieldName);
 			}
+		});
 
-			if (config.cache && this.changes.has(formId)) this.saveCache(formId);
+		if (Object.hasOwn(config, 'hasQuill') && this.quillInstances.has(formId)) {
+			const instances = this.quillInstances.get(formId);
+			instances.forEach(quillInstance => {
+				// Disable the editor
+				quillInstance.disable();
 
-			// Cleanup items
-			for (let [id, input] of this.inputs.entries()) {
-				if (input.form === formId) {
-					this.inputs.delete(id);
+				// Remove all event listeners
+				quillInstance.off('text-change');
+				quillInstance.off('selection-change');
+
+				// Get the container elements
+				const container = quillInstance.container.parentElement;
+				const toolbar = container?.querySelector('.ql-toolbar');
+
+				// Remove toolbar
+				if (toolbar) {
+					toolbar.remove();
 				}
-			}
-			// Clean up dependencies for this form
-			this.dependencies.forEach((dependency, fieldName) => {
-				dependency.items = dependency.items.filter(item => item.form !== formId);
 
-				// Remove the dependency entry entirely if no items left
-				if (dependency.items.length === 0) {
-					this.dependencies.delete(fieldName);
+				// Clear the editor content
+				quillInstance.setText('');
+
+				// Remove container
+				if (container && container.classList.contains('editor-container')) {
+					const textarea = container.nextElementSibling;
+					if (textarea?.tagName === 'TEXTAREA') {
+						textarea.style.display = '';
+					}
+					container.remove();
 				}
 			});
 
-			if (Object.hasOwn(config, 'hasQuill') && this.quillInstances.has(formId)) {
-				const instances = this.quillInstances.get(formId);
-				instances.forEach(quillInstance => {
-					// Disable the editor
-					quillInstance.disable();
-
-					// Remove all event listeners
-					quillInstance.off('text-change');
-					quillInstance.off('selection-change');
-
-					// Get the container elements
-					const container = quillInstance.container.parentElement;
-					const toolbar = container?.querySelector('.ql-toolbar');
-
-					// Remove toolbar
-					if (toolbar) {
-						toolbar.remove();
+			this.quillInstances.delete(formId);
+		}
+		let checks = {
+			repeater: this.repeaters,
+			tagList: this.tagLists,
+			charLimit: this.charLimits,
+			quantity: this.quantityFields
+		};
+		for (let [type, check] of Object.entries(checks)) {
+			if (check.size === 0) continue;
+			let hasAny = Array.from(check.values()).filter(item => item.form === formId);
+			if (hasAny.length > 0) {
+				hasAny.forEach(item => {
+					switch (type) {
+						case 'repeater':
+							this.removeRepeaterListeners(item.element);
+							break;
+						case 'tagList':
+							this.removeTagListListeners(item.element);
+							break;
+						case 'charLimit':
+							this.removeCharacterLimitListeners(item.element);
+							break;
+						case 'quantity':
+							this.removeQuantityListeners(item.element);
+							break;
 					}
 
-					// Clear the editor content
-					quillInstance.setText('');
-
-					// Remove container
-					if (container && container.classList.contains('editor-container')) {
-						const textarea = container.nextElementSibling;
-						if (textarea?.tagName === 'TEXTAREA') {
-							textarea.style.display = '';
-						}
-						container.remove();
+					if (check.has(item.id)) {
+						check.delete(item.id);
 					}
 				});
-
-				this.quillInstances.delete(formId);
 			}
-			let checks = {
-				repeater: this.repeaters,
-				tagList: this.tagLists,
-				charLimit: this.charLimits,
-				quantity: this.quantityFields
-			};
-			for (let [type, check] of Object.entries(checks)) {
-				if (check.size === 0) continue;
-				let hasAny = Array.from(check.values()).filter(item => item.form === formId);
-				if (hasAny.length > 0) {
-					hasAny.forEach(item => {
-						switch (type) {
-							case 'repeater':
-								this.removeRepeaterListeners(item.element);
-								break;
-							case 'tagList':
-								this.removeTagListListeners(item.element);
-								break;
-							case 'charLimit':
-								this.removeCharacterLimitListeners(item.element);
-								break;
-							case 'quantity':
-								this.removeQuantityListeners(item.element);
-								break;
-						}
-
-						if (check.has(item.id)) {
-							check.delete(item.id);
-						}
-					});
-				}
-			}
-
-
-			this.removeFormListeners(config.element);
-			this.forms.delete(formId);
-
-			window.debouncer.cancel(`form_changes`);
-		}
-		defineSummaryTemplate() {
-			this.summaryTemplate = true;
-			let form = this;
-			this.templates.define(
-				'formSummary',
-				{
-					refs: {
-						result: '.result',
-						h3: 'h3',
-						p: 'p',
-					},
-					setup({ el, refs, manyRefs, data }) {
-						const skipFields = ['sendAll', ...data.config.options.ignore??[]];
-
-						for (let [key, value] of Object.entries(data.changes)) {
-							if (skipFields.includes(key) || form.isEmptyValue(value)) continue;
-
-							let input = Array.from(form.inputs.values())
-								.find(temp => temp.field?.dataset.field === key);
-							if (!input) continue;
-
-							let entry = refs.result.cloneNode(true);
-							let title = entry.querySelector('h3');
-							let p = entry.querySelector('p');
-
-							// Get field label - prioritize legend for fieldsets, then label
-							const legend = input.field?.querySelector('legend');
-							title.textContent = legend
-								? legend.textContent.replace('*', '').trim()
-								: input.ui.label?.textContent.replace('*', '').trim();
-
-
-							const formattedValue = form.formatValueForSummary(value, input);
-
-							if (formattedValue instanceof HTMLElement) {
-								// If it's an HTML element (repeater, tag-list, etc.), replace <p>
-								p.replaceWith(formattedValue);
-							} else {
-								// If it's a string, set text content
-								p.textContent = formattedValue;
-							}
-
-							el.append(entry);
-						}
-						let uploads = data.config?.element?.querySelectorAll('[data-upload-field]');
-						if (uploads) {
-							uploads.forEach(upload => {
-								let label = upload.querySelector('h2')?.textContent??'Upload:';
-								let imgs = upload.querySelectorAll('.item-grid.preview img');
-								let field = refs.result.cloneNode(true);
-								if (imgs) {
-									let entry = refs.result.cloneNode(true);
-									let title = field.querySelector('h3');
-									let p = field.querySelector('p');
-									p?.remove();
-									if (title) title.textContent = label;
-									imgs.forEach(img => {
-										img = img.cloneNode(true);
-										entry.append(img);
-									});
-									el.append(entry);
-								}
-							});
-						}
-
-						refs.result?.remove();
-						data.config.element.after(el);
-						window.fade(data.config.element, false);
-					}
-				}
-			);
 		}
 
 
-		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)
-			};
+		this.removeFormListeners(config.element);
+		this.forms.delete(formId);
 
-			for (const [selector, handler] of Object.entries(fieldHandlers)) {
-				if (container.querySelector(selector)) {
-					handler();
-				}
-			}
+		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??[]];
 
-			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);
-				}
+					for (let [key, value] of Object.entries(data.changes)) {
+						if (skipFields.includes(key) || form.isEmptyValue(value)) continue;
 
-				if (!this.quillInstances.has(config.id)) {
-					this.quillInstances.set(config.id, new Set());
-				}
+						let input = Array.from(form.inputs.values())
+							.find(temp => temp.field?.dataset.field === key);
+						if (!input) continue;
 
-				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--;
+						let entry = refs.result.cloneNode(true);
+						let title = entry.querySelector('h3');
+						let p = entry.querySelector('p');
+
+						// Get field label - prioritize legend for fieldsets, then label
+						const legend = input.field?.querySelector('legend');
+						title.textContent = legend
+							? legend.textContent.replace('*', '').trim()
+							: input.ui.label?.textContent.replace('*', '').trim();
+
+
+						const formattedValue = form.formatValueForSummary(value, input);
+
+						if (formattedValue instanceof HTMLElement) {
+							// If it's an HTML element (repeater, tag-list, etc.), replace <p>
+							p.replaceWith(formattedValue);
+						} else {
+							// If it's a string, set text content
+							p.textContent = formattedValue;
+						}
+
+						el.append(entry);
 					}
-					if (change === 0) return;
-					let field = this.getField(e.target);
-					let step = conf.ui.input.step;
-					step = Math.max(step, 1);
-					if (e.ctrlKey && e.shiftKey) {
-						step = step * 50;
-					} else if (e.ctrlKey) {
-						step = step *5;
-					} else if (e.shiftKey) {
-						step = step * 10;
-					}
-					let value = (conf.ui.input.value === '') ? 0 : parseFloat(conf.ui.input.value);
-					conf.ui.input.value = (value + (step * change));
-
-					value = parseFloat(conf.ui.input.value);
-
-					if (conf.ui.input.min && value < conf.ui.input.min) {
-						conf.ui.input.value = conf.ui.input.min;
-						conf.ui.decrease.disabled = true;
-					} else if (conf.ui.input.max && value > conf.ui.input.max) {
-						conf.ui.input.value = conf.ui.input.max;
-						conf.ui.increase.disabled = true;
-					} else {
-						if (conf.ui.decrease.disabled) conf.ui.decrease.disabled = false;
-						if (conf.ui.increase.disabled) conf.ui.increase.disabled = false;
-					}
-				}
-			checkForRepeaters(form) {
-
-				if (!form.querySelector(this.selectors.repeater.repeater)) return;
-
-				form.querySelectorAll(this.selectors.repeater.repeater).forEach(repeater => {
-					let config = {
-						id: repeater.querySelector('template').className??window.generateID('repeater'),
-						ui: window.uiFromSelectors(this.selectors.repeater, repeater),
-						form: form.dataset.formId,
-						element: repeater,
-						field: this.getField(repeater),
-						sortable: false,
-						rows: []
-					};
-
-					if (!config.ui.add) return;
-
-					let template = repeater.querySelector('template');
-					this.templates.define(
-						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);
+					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);
 								});
-							}
-						},
-					);
-
-					if (window.Sortable) {
-						config.sortable = new Sortable(repeater, {
-							handle: this.selectors.repeater.header,
-							animation: 150,
-							onEnd: () => {
-								this.reindexList(repeater);
+								el.append(entry);
 							}
 						});
 					}
 
-					repeater.dataset.repeaterId = config.id;
-					this.addRepeaterListeners(repeater);
-					this.repeaters.set(config.id, config);
-				});
-
+					refs.result?.remove();
+					data.config.element.after(el);
+					window.fade(data.config.element, false);
+				}
 			}
-				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;
+	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)
+		};
 
-					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);
-				});
-
+		for (const [selector, handler] of Object.entries(fieldHandlers)) {
+			if (container.querySelector(selector)) {
+				handler();
 			}
-				addTagListListeners(el) {
-					el.addEventListener('click', this.tagListClick);
-					el.addEventListener('keypress', this.tagListInput);
-				}
-				removeTagListListeners(el) {
-					el.removeEventListener('click', this.tagListClick);
-					el.removeEventListener('keypress', this.tagListInput);
-				}
+		}
 
-				handleTagListClick(e) {
-					if (window.targetCheck(e,this.selectors.tagList.add)) {
-						this.addTagListItem(e.target.closest('[data-tag-list-id]'));
-					} else if (window.targetCheck(e, this.selectors.tagList.remove)) {
-						this.removeTagListItem(e.target.closest(this.selectors.tagList.item));
+		let inputs = Array.from(container.querySelectorAll(this.inputSelectors))
+			.filter(input => !input.closest('.ql-clipboard'));
+		inputs.map(input => {
+			this.getItem(input, config?.id);
+		});
+	}
+	checkForQuill(form, config) {
+		if (!form.querySelector('[data-editor]')) return;
+		if (config && !Object.hasOwn(config, 'hasQuill')){
+			config.hasQuill = true;
+			this.forms.set(config.id, config);
+		}
+
+		if (!this.quillInstances.has(config.id)) {
+			this.quillInstances.set(config.id, new Set());
+		}
+
+		const instances = window.jvbQuill(form);
+		instances.forEach(instance => {
+			this.quillInstances.get(config.id).add(instance);
+		});
+	}
+	checkForQuantity(form) {
+		if (!form.querySelector(this.selectors.number.number)) return;
+		form.querySelectorAll(this.selectors.number.number).forEach(num => {
+			let config = {
+				id: window.generateID('quant'),
+				form: form.dataset.formId,
+				ui: window.uiFromSelectors(this.selectors.number, num),
+				element: num
+			};
+			num.dataset.numId = config.id;
+			this.quantityFields.set(config.id, config);
+			this.addQuantityListeners(num);
+		});
+	}
+	addQuantityListeners(el) {
+		el.addEventListener('click', this.quantityClick);
+	}
+	removeQuantityListeners(el) {
+		el.removeEventListener('click', this.quantityClick);
+	}
+	handleQuantityClick(e) {
+		let conf = this.quantityFields.get(e.target.closest('[data-num-id]')?.dataset.numId);
+		if(!conf) return;
+		let change = 0;
+		if (conf.ui.increase.contains(e.target)) {
+			change++;
+		} else if (conf.ui.decrease.contains(e.target)) {
+			change--;
+		}
+		if (change === 0) return;
+		let field = this.getField(e.target);
+		let step = conf.ui.input.step;
+		step = Math.max(step, 1);
+		if (e.ctrlKey && e.shiftKey) {
+			step = step * 50;
+		} else if (e.ctrlKey) {
+			step = step *5;
+		} else if (e.shiftKey) {
+			step = step * 10;
+		}
+		let value = (conf.ui.input.value === '') ? 0 : parseFloat(conf.ui.input.value);
+		conf.ui.input.value = (value + (step * change));
+
+		value = parseFloat(conf.ui.input.value);
+
+		if (conf.ui.input.min && value < conf.ui.input.min) {
+			conf.ui.input.value = conf.ui.input.min;
+			conf.ui.decrease.disabled = true;
+		} else if (conf.ui.input.max && value > conf.ui.input.max) {
+			conf.ui.input.value = conf.ui.input.max;
+			conf.ui.increase.disabled = true;
+		} else {
+			if (conf.ui.decrease.disabled) conf.ui.decrease.disabled = false;
+			if (conf.ui.increase.disabled) conf.ui.increase.disabled = false;
+		}
+	}
+	checkForRepeaters(form) {
+
+		if (!form.querySelector(this.selectors.repeater.repeater)) return;
+
+		form.querySelectorAll(this.selectors.repeater.repeater).forEach(repeater => {
+			let config = {
+				id: repeater.querySelector('template').className??window.generateID('repeater'),
+				ui: window.uiFromSelectors(this.selectors.repeater, repeater),
+				form: form.dataset.formId,
+				element: repeater,
+				field: this.getField(repeater),
+				sortable: false,
+				rows: []
+			};
+
+			if (!config.ui.add) return;
+
+			let template = repeater.querySelector('template');
+			this.templates.define(
+				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);
+						});
 					}
-				}
-			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;
+			if (window.Sortable) {
+				config.sortable = new Sortable(repeater, {
+					handle: this.selectors.repeater.header,
+					animation: 150,
+					onEnd: () => {
+						this.reindexList(repeater);
 					}
-
-					const fieldName = input.name.replace('new_','');
-					data[fieldName] = value;
-				}
-
-				// Stop if validation failed
-				if (!isValid) {
-					this.a11y.announce('Please correct the errors before adding');
-					const firstInvalid = config.ui.inputs.find(input => {
-						const isRequired = input.required || input.dataset.required === 'true';
-						return (isRequired && !this.getFieldValue(input));
-					});
-					if (firstInvalid) firstInvalid.focus();
-					return;
-				}
-
-				if (!hasValue) {
-					this.a11y.announce('Please fill in at least one field');
-					config.ui.inputs[0].focus();
-					return;
-				}
-
-				// Build label
-				let label;
-				switch (config.format) {
-					case 'first_field':
-						label = Object.values(data)[0];
-						break;
-					case 'all_fields':
-						label = Object.values(data).join(', ');
-						break;
-					default:
-						if (config.format.includes('{')) {
-							label = config.format;
-							for (const [key, value] of Object.entries(data)) {
-								label = label.replace(`{${key}}`, value);
-							}
-						} else {
-							label = data[config.format]??Object.values(data)[0];
-						}
-						break;
-				}
-
-				let newItem = this.templates.create(tagList.dataset.tagListId, {
-					label: label,
-					fieldName: config.fieldName
-				});
-
-				const index = config.ui.items?.children?.length ?? 0;
-				newItem?.querySelectorAll('input[type=hidden]')?.forEach(input => {
-					const fieldKey = input.dataset.field;
-					input.name = `${config.fieldName}:${index}:${fieldKey}`;
-					input.id = `${config.fieldName}:${index}:${fieldKey}`;
-					input.value = data[fieldKey] || '';
-				});
-
-				config.ui.items.append(newItem);
-
-				// Clear inputs AFTER success
-				for (let input of config.ui.inputs) {
-					if (['checkbox', 'radio'].includes(input.type)) {
-						input.checked = false;
-					} else {
-						input.value = '';
-					}
-					this.clearValidation(input);
-				}
-
-				config.ui.inputs[0]?.focus();
-				this.updateCollectionField(tagList);
-				this.a11y.announce('Item added');
-			}
-				removeTagListItem(item) {
-					let tagList = item.closest('[data-tag-list-id]');
-					if (!tagList) return;
-					item.remove();
-					this.reindexList(tagList);
-					this.updateCollectionField(tagList);
-					this.a11y.announce('Item removed');
-				}
-				handleTagListInput(e) {
-					let target = e.target;
-					let field = target.closest('[data-tag-list-id]');
-					if (!field) return;
-					let config = this.tagLists.get(field.dataset.tagListId);
-					if (!config) return;
-
-					if (e.key === 'Enter') {
-						if (target === config.ui.inputs[config.ui.inputs.length - 1]) {
-							e.preventDefault();
-							this.addTagListItem(target.closest('[data-tag-list-id]'));
-						} else {
-							e.preventDefault();
-							let index = config.ui.inputs.indexOf(target);
-							config.ui.inputs[index+1].focus();
-						}
-					}
-
-				}
-
-			checkForConditionalFields(form) {
-				form.querySelectorAll(this.selectors.dependsOn).forEach( field => {
-					const dependsOn = field.dataset.dependsOn;
-					const requiredValue = field.dataset.dependsValue;
-					const operator = field.dataset.dependsOperatior??'==';
-
-					if (!this.dependencies.has(dependsOn)) {
-						let element = document.querySelector(`[field="${dependsOn}"]`);
-						if (element) {
-							this.dependencies.set(dependsOn, {
-								element: element,
-								items: []
-							});
-						}
-					}
-					let dependency = this.dependencies.get(dependsOn);
-					dependency.items.push({
-						field: field,
-						form: form.dataset.formId,
-						requiredValue: requiredValue,
-						operator: operator
-					});
-					this.dependencies.set(dependsOn, dependency);
-					this.checkFieldDependency(dependency, dependsOn);
 				});
 			}
-				checkFieldDependency(dependentField, controlFieldName) {
-					const controlField = this.dependencies.get(controlFieldName);
-					if (!controlField) return;
 
-					const controlValue = this.getFieldCheckedValue(controlField.element);
-					const shouldShow = this.evaluateCondition(
-						controlValue,
-						dependentField.requiredValue,
-						dependentField.operator
-					);
+			repeater.dataset.repeaterId = config.id;
+			this.addRepeaterListeners(repeater);
+			this.repeaters.set(config.id, config);
+		});
 
-					this.toggleFieldVisibility(dependentField.field, shouldShow);
-				}
-				evaluateCondition(value, requiredValue, operator) {
-					const fieldStr = String(value || '');
-					const requiredStr = String(requiredValue || '');
+	}
+	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);
 
-					switch (operator) {
-						case '==': return fieldStr === requiredStr;
-						case '!=': return fieldStr !== requiredStr;
-						case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
-						case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
-						case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
-						case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
-						case 'contains': return fieldStr.includes(requiredStr);
-						case 'empty': return fieldStr === '';
-						case 'not_empty': return fieldStr !== '';
-						default: return fieldStr === requiredStr;
-					}
-				}
-				toggleFieldVisibility(field, show) {
-					const wrapper = field.closest('.field, fieldset');
-					if (!wrapper) return;
+		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);
 
-					wrapper.hidden = !show;
-					wrapper.querySelectorAll('input, select, textarea').forEach(control => {
-						control.disabled = !show;
-						if (!show && control.hasAttribute('required')) {
-							control.dataset.wasRequired = 'true';
-							control.removeAttribute('required');
-						} else if (show && control.dataset.wasRequired === 'true') {
-							control.setAttribute('required', '');
-							delete control.dataset.wasRequired;
+		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);
+	}
+
+	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.dependsOperatior??'==';
+
+			let formData = this.forms.get(form.dataset.formId);
+
+			if (!this.dependencies.has(dependsOn)) {
+				if (Object.hasOwn(formData.ui.fields, dependsOn)) {
+					this.dependencies.set(dependsOn, []);
+				}
+			}
+			let dependency = this.dependencies.get(dependsOn);
+			if (dependency) {
+				dependency.push({
+					field:	field,
+					form: form.dataset.formId,
+					requiredValue: requiredValue,
+					operator: operator
+				});
+				this.dependencies.set(dependsOn, dependency);
+			}
+
+			this.checkFieldDependency(field, dependsOn);
+		});
+	}
+	checkFieldDependency(dependentField, controlFieldName) {
+		const form = this.getForm(dependentField);
+		const controlField = this.dependencies.get(controlFieldName);
+		if (!controlField) return;
+
+
+		const controlValue = this.getFieldValue(form.ui.fields[controlFieldName]);
+		const shouldShow = this.evaluateCondition(
+			controlValue,
+			dependentField.dataset.dependsValue,
+			dependentField.dataset.dependsOperatior
+		);
+
+		this.toggleFieldVisibility(dependentField, shouldShow);
+	}
+	evaluateCondition(value, requiredValue, operator) {
+		const fieldStr = String(value || '');
+		const requiredStr = String(requiredValue || '');
+
+		switch (operator) {
+			case '==': return fieldStr === requiredStr;
+			case '!=': return fieldStr !== requiredStr;
+			case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
+			case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
+			case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
+			case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
+			case 'contains': return fieldStr.includes(requiredStr);
+			case 'empty': return fieldStr === '';
+			case 'not_empty': return fieldStr !== '';
+			default: return fieldStr === requiredStr;
+		}
+	}
+	toggleFieldVisibility(field, show) {
+		const wrapper = field.closest('.field, fieldset');
+		if (!wrapper) return;
+
+		wrapper.hidden = !show;
+		wrapper.querySelectorAll('input, select, textarea').forEach(control => {
+			control.disabled = !show;
+			if (!show && control.hasAttribute('required')) {
+				control.dataset.wasRequired = 'true';
+				control.removeAttribute('required');
+			} else if (show && control.dataset.wasRequired === 'true') {
+				control.setAttribute('required', '');
+				delete control.dataset.wasRequired;
+			}
+		});
+	}
 	checkForCharacterLimits(form) {
 		if (!form.querySelector(this.selectors.limits.hasLimit)) return;
 		this.countUpdaters = this.updateCount.bind(this);
@@ -1189,84 +1247,92 @@
 			this.addCharacterLimitListeners(input);
 		});
 	}
-				addCharacterLimitListeners(input) {
-					input.addEventListener('input', this.countUpdaters, {passive: true});
+	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);
 				}
-				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);
+			});
+			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
+						);
 					}
 				}
-			checkForImageUploads(form, config) {
-				window.jvbUploads.scanFields(form, config.options.autoUpload, config.options.imageMeta);
-			}
+			});
+			this.forms.set(config.id, config);
+		}
+	}
+	validateStep(section, config) {
+		const formId = section.closest('[data-form-id]')?.dataset.formId;
+		if (!formId) return true;
 
-			checkForTabs(form, config) {
-				if (window.jvbTabs && form.querySelector('nav.tabs')) {
-					config.tabs = window.jvbTabs.registerTab(form, {
-						preCheck: (section, tabConfig) => {
-							return this.validateStep(section, config);
-						}
-					});
-					config.ui.tabs = window.uiFromSelectors(this.selectors.tabs, form);
-					config.ui.tabs.sections = Array.from(form.querySelectorAll(this.selectors.tabs.sections));
-					config.ui.tabs.inputs = {};
-					config.ui.tabs.sections.forEach(section => {
-						config.ui.tabs.inputs[section.dataset.tab] = Array.from(section.querySelectorAll(this.inputs));
-					});
-					config.ui.tabs.buttons = Array.from(form.querySelectorAll(this.selectors.tabs.buttons));
+		const form = this.forms.get(formId);
+		if (!form) return true;
 
-					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;
+		const inputs = Array.from(this.inputs.values())
+			.filter(item =>
+				item &&
+				item.form === formId &&
+				item.section === section.dataset.tab &&
+				!item.element.closest('[hidden]')
+			);
 
-								window.showProgress(
-									config.ui.tabs.progress,
-									step,
-									total
-								);
-							}
-						}
-					});
-					this.forms.set(config.id, config);
-				}
-			}
-			validateStep(section, config) {
-				const formId = section.closest('[data-form-id]')?.dataset.formId;
-				if (!formId) return true;
-
-				const form = this.forms.get(formId);
-				if (!form) return true;
-
-				const inputs = Array.from(this.inputs.values())
-					.filter(item =>
-						item &&
-						item.form === formId &&
-						item.section === section.dataset.tab &&
-						!item.element.closest('[hidden]')
-					);
-
-				return inputs.every(item => this.validateField(item.element) === true);
-			}
-			checkForSelectors(form) {
-				if (window.jvbSelector) window.jvbSelector.scanExistingFields(form);
-			}
+		return inputs.every(item => this.validateField(item.element) === true);
+	}
+	checkForSelectors(form) {
+		if (window.jvbSelector) window.jvbSelector.scanExistingFields(form);
+	}
 	/**
 	 * Mainly for repeaters or taglist
 	 * @param {HTMLElement} container
@@ -1322,7 +1388,7 @@
 	}
 	/**********************************************************************
 	 VALIDATION
-	**********************************************************************/
+	 **********************************************************************/
 	//text, email, url, tel, date, time, datetime, number
 	//select, checkbox, radio, true_false
 	//textarea
@@ -1357,8 +1423,6 @@
 		field.classList.remove('has-success');
 		field.classList.add('has-error');
 
-		if (item.ui.success) item.ui.success.hidden = true;
-		if (item.ui.error) item.ui.error.hidden = true;
 		if (item.ui.message) {
 			item.ui.message.hidden = false;
 			item.ui.message.textContent = message;
@@ -1374,9 +1438,6 @@
 		field.classList.remove('has-error');
 		field.classList.add('has-success');
 
-		if (item.ui.success) item.ui.success.hidden = false;
-		if (item.ui.error) item.ui.error.hidden = true;
-
 		if (item.ui.message) {
 			item.ui.message.hidden = message=== '';
 			item.ui.message.textContent = message;
@@ -1524,35 +1585,35 @@
 		form.ui.status.icon.className = 'icon icon-'+this.getDefaultIcon(status);
 		setTimeout(()=> form.ui.status.status.hidden = true, (status === 'submitted') ? 3000 : 10000);
 	}
-		getDefaultMessage(status) {
-			const messages = {
-				'saving': 'Saving changes...',
-				'autosaved': 'Changes saved locally. Submit form to send to server.',
-				'uploading': 'Uploading your form to server',
-				'submitted': 'Successfully sent to server',
-				'pending': 'Unsaved changes',
-				'restored': 'Welcome back! We\'ve restored your previous entry.',
-				'error': 'Failed to save changes. Refresh and try again?',
-				'offline': 'Changes will be saved when online'
-			};
-			return messages[status]??status;
+	getDefaultMessage(status) {
+		const messages = {
+			'saving': 'Saving changes...',
+			'autosaved': 'Changes saved locally. Submit form to send to server.',
+			'uploading': 'Uploading your form to server',
+			'submitted': 'Successfully sent to server',
+			'pending': 'Unsaved changes',
+			'restored': 'Welcome back! We\'ve restored your previous entry.',
+			'error': 'Failed to save changes. Refresh and try again?',
+			'offline': 'Changes will be saved when online'
+		};
+		return messages[status]??status;
+	}
+	getDefaultIcon(status) {
+		const icons = {
+			'autosaved': 'check-circle',
+			'submitted': 'check-circle',
+			'restored': 'history',
+			'error': 'close-circle',
+			'offline': 'cloud-slash',
+			'pending': 'exclamation-mark'
 		}
-		getDefaultIcon(status) {
-			const icons = {
-				'autosaved': 'check-circle',
-				'submitted': 'check-circle',
-				'restored': 'history',
-				'error': 'close-circle',
-				'offline': 'cloud-slash',
-				'pending': 'exclamation-mark'
-			}
-			return icons[status]??'';
-		}
+		return icons[status]??'';
+	}
 
 
 	/**********************************************************************
 	 SUMMARY
-	**********************************************************************/
+	 **********************************************************************/
 	showSummary(data) {
 		let summary = this.templates.create('formSummary', data);
 		data.config.element.after(summary);
@@ -1560,7 +1621,7 @@
 	}
 	/**********************************************************************
 	 UTILITY
-	**********************************************************************/
+	 **********************************************************************/
 	getForm(element) {
 		let form = element.closest('[data-form-id]');
 		if (!form) return false;
@@ -1581,6 +1642,7 @@
 	getFieldValue(element) {
 		let type = this.getFieldType(element);
 		let conf = this.getItem(element);
+
 		let fieldName = conf.field?.dataset.field??false;
 		if (!fieldName) return false;
 
@@ -1592,8 +1654,8 @@
 				return this.getTagListValue(element, conf);
 
 			case 'group':
-				//Do we actually need anything here? I think each subfield just
-				break;
+				return null;
+			//Do we actually need anything here? I think each subfield just
 
 			case 'location':
 				return this.getLocationValue(element, conf);
@@ -1659,25 +1721,25 @@
 		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);
-						}
+	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;
-		}
+			value.push(rowData);
+		});
+		return value;
+	}
 	getFieldInput(field) {
 		// For quill fields, target the specific editor textarea
 		const quillTextarea = field.querySelector('textarea[data-editor]');
@@ -1685,40 +1747,46 @@
 
 		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 (!conf.value) {
-			conf.value = conf.field?.querySelector(`input[type=hidden][name="${fieldName}"]`)
-				|| conf.field?.querySelector(`input[type=hidden]`);
+	getTagListValue(element, conf) {
+		if (!conf.container) {
+			conf.container = conf.field?.querySelector('.tag-items');
 			this.saveItem(conf);
 		}
-		return conf.value?.value ?? '';
+		let value = [];
+		Array.from(conf.container.children).forEach(item => {
+			let inputs = item.querySelectorAll('input[type="hidden"]');
+			let fieldData = {};
+			inputs.forEach(input => {
+				fieldData[input.dataset.field] = input.value;
+			});
+			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;
 	}
 
 	/**
@@ -1761,7 +1829,7 @@
 			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
+							   // These might need special handling depending on your needs
 				return this.formatHiddenFieldForSummary(value, input, fieldType);
 
 			default:
@@ -1981,7 +2049,7 @@
 	}
 	/**********************************************************************
 	 Subscription
-	**********************************************************************/
+	 **********************************************************************/
 	subscribe(callback) {
 		this.subscribers.add(callback);
 		return () => this.subscribers.delete(callback);
@@ -1998,7 +2066,7 @@
 	}
 	/**********************************************************************
 	 Cleanup
-	**********************************************************************/
+	 **********************************************************************/
 	destroy() {
 		if (this.forms.size > 0) {
 			Array.from(this.forms.values()).forEach(form => {

--
Gitblit v1.10.0