From 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Nov 2025 07:42:23 +0000
Subject: [PATCH] =Feed block mostly good! Referrals look good to go. Ready for Madi and Heidi to approve

---
 assets/js/concise/FormController.js |  509 ++++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 385 insertions(+), 124 deletions(-)

diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index cdae988..0e53476 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -3,19 +3,27 @@
  * Works with DataStore for CRUD operations and standalone for front-end forms
  */
 class FormController {
-	constructor() {
-		this.store = new window.jvbStore({
-			name:'forms',
-			storeName: 'forms',
-			keyPath: 'formId',
-			indexes: [
-				{ name: 'status', keyPath: 'status' },
-				{ name: 'operationId', keyPath: 'operationId' },
-				{ name: 'timestamp', keyPath: 'timestamp' },
-				{ name: 'formType', keyPath: 'type' }
-			],
-			TTL: 604800000, //7 days
-		});
+	constructor(config = {}) {
+		this.config = {
+			collectFormData: false,
+			... config
+		}
+		const store = window.jvbStore.register(
+			'forms',
+			{
+				storeName: 'forms',
+				keyPath: 'formId',
+				indexes: [
+					{ name: 'status', keyPath: 'status' },
+					{ name: 'operationId', keyPath: 'operationId' },
+					{ 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;
 
@@ -49,6 +57,10 @@
 			remove: 800,
 			reorder: 1000
 		};
+		this.isTimeline = false;
+		if (window.crudManager && window.crudManager.isTimeline) {
+			this.isTimeline = true;
+		}
 
 		// Bind handlers
 		this.clickHandler = this.handleClick.bind(this);
@@ -57,18 +69,49 @@
 		this.inputHandler = this.handleInput.bind(this);
 		this.focusHandler = this.handleFocus.bind(this);
 		this.blurHandler = this.handleBlur.bind(this);
+		//Processors
+		this.processRepeaterField = this.processRepeaterField.bind(this);
+		this.processGroupField = this.processGroupField.bind(this);
+		this.processLocationField = this.processLocationField.bind(this);
+		this.processRegularField = this.processRegularField.bind(this);
 
 		this.init();
 	}
 
 	async init() {
-		// Check for pending operations on page load
-		await this.checkPendingOperations();
-
 		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);
+				}
+			});
+		}
+	}
+
+	/**
+	 * 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);
+			}
+		}
+
+		// 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) {
@@ -84,16 +127,21 @@
 		}
 	}
 
-	async checkPendingForms() {
-		let items = await this.store.query('status', 'draft');
-		items.forEach(item => {
-			let form = this.forms.get(item.formId);
-			if (form && form.element) {
-				form.element.querySelector('.restore-form').hidden = false;
+	checkPendingForms() {
+		// No async needed - data is already loaded in memory
+		const allForms = this.store.getAll();
+		const pendingForms = allForms.filter(form => form.status === 'draft');
+
+		pendingForms.forEach(item => {
+			const form = this.forms.get(item.formId);
+			if (form?.element) {
+				const restoreBtn = form.element.querySelector('.restore-form');
+				if (restoreBtn) {
+					restoreBtn.hidden = false;
+				}
 				new this.populateForm(form.element, item.data);
 			}
 		});
-
 	}
 	/**
 	 * Check for pending operations from previous session
@@ -113,28 +161,31 @@
 	/**
 	 * Show notification for pending changes
 	 */
-	showPendingNotification(pendingData) {
-		const formElement = document.querySelector(`[data-form-id="${pendingData.formId}"]`);
+	/**
+	 * 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="${pendingData.formId}">Restore</button>
-			<button class="discard-changes" data-form-id="${pendingData.formId}">Discard</button>
-		`;
+        <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', () => {
-			this.restorePendingForm(pendingData);
+		notification.querySelector('.restore-changes').addEventListener('click', async () => {
+			await this.restorePendingForm(formId, formData);
 			notification.remove();
 		});
 
-		notification.querySelector('.discard-changes').addEventListener('click', () => {
-			this.discardPendingForm(pendingData.formId);
+		notification.querySelector('.discard-changes').addEventListener('click', async () => {
+			await this.discardPendingForm(formId);
 			notification.remove();
 		});
 	}
@@ -142,16 +193,20 @@
 	/**
 	 * Restore pending form data
 	 */
-	restorePendingForm(pendingData) {
-		const form = document.querySelector(`[data-form-id="${pendingData.formId}"]`);
+	async restorePendingForm(formId, formData) {
+		const form = document.querySelector(`[data-form-id="${formId}"]`);
 		if (!form) return;
 
 		// Populate form with cached data
-		new this.populateForm(form, pendingData.formData);
+		new this.populateForm(form, formData);
 
-		// Mark as restored
-		pendingData.status = 'restored';
-		this.pendingForms.set(pendingData.formId, pendingData);
+		// 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');
@@ -162,10 +217,14 @@
 	 * Discard pending form data
 	 */
 	async discardPendingForm(formId) {
-		this.store.delete(formId);
+		try {
+			await this.store.delete(formId);
 
-		if (window.jvbA11y) {
-			window.jvbA11y.announce('Previous changes discarded');
+			if (window.jvbA11y) {
+				window.jvbA11y.announce('Previous changes discarded');
+			}
+		} catch (error) {
+			console.error('Failed to discard pending form:', error);
 		}
 	}
 
@@ -188,6 +247,7 @@
 	 * Register a standalone form (for front-end forms)
 	 */
 	registerForm(formElement, options = {}) {
+		if (!formElement) return;
 		const formId = formElement.dataset.formId || `form_${Date.now()}`;
 		formElement.dataset.formId = formId;
 
@@ -196,16 +256,17 @@
 		const formConfig = {
 			element: formElement,
 			id: formId,
+			status: '',
 			options: {
-				autoSave: true,
+				autosave: 'autosave' in formElement.dataset,
 				saveDelay: this.autoSaveDefaults.delay,
-				endpoint: formElement.dataset.save,
+				endpoint: formElement.dataset.save??'',
+				formStatus: true,
 				cache: true,
 				...options
 			},
 			dependencies: new Map(),
-			data: this.collectFormData(formElement),
-			isDirty: false
+			data: this.collectFormData(formElement, true),
 		};
 
 		// Initialize special fields
@@ -255,7 +316,7 @@
 
 		// Scan for existing selector fields
 		if (window.jvbSelector) {
-			window.jvbSelector.scanExistingFields();
+			window.jvbSelector.scanExistingFields(form);
 		}
 	}
 
@@ -444,8 +505,7 @@
 
 		container.appendChild(row);
 
-		// Schedule save if auto-save enabled
-		if (formConfig && formConfig.options.autoSave) {
+		if (formConfig) {
 			this.scheduleSave(formConfig, {
 				type: 'repeater',
 				action: 'add',
@@ -472,7 +532,7 @@
 		this.updateRepeaterOrder(repeater, formConfig);
 
 		// Schedule save
-		if (formConfig && formConfig.options.autoSave) {
+		if (formConfig) {
 			this.scheduleSave(formConfig, {
 				type: 'repeater',
 				action: 'remove',
@@ -515,7 +575,7 @@
 		});
 
 		// Schedule save
-		if (formConfig && formConfig.options.autoSave) {
+		if (formConfig) {
 			this.scheduleSave(formConfig, {
 				type: 'repeater',
 				action: 'reorder',
@@ -650,23 +710,171 @@
 
 	/* ========== Event Handlers ========== */
 
-	handleSubmit(event) {
-		//TODO: submit data, if successful, delete from store
-		if (this.subscribers.size > 0 ){
-			const form = event.target;
-			if (!form.dataset.formId) return;
+	async handleSubmit(event) {
+		const form = event.target;
+		if (!form.dataset.formId) return;
+
+		const formConfig = this.forms.get(form.dataset.formId);
+
+		// Handle subscriber-based forms
+		if (this.subscribers.size > 0) {
 			event.preventDefault();
-
-			const formConfig = this.forms.get(form.dataset.formId);
-			if (!formConfig) return;
-
 			const formData = this.collectFormData(form);
+
+			// Notify subscribers (they'll handle actual submission)
 			this.notify('form-submit', {
-				formId: formConfig.id,
-				data: formData,
+				formId: form.dataset.formId,
+				fullData: formData,
 				config: formConfig
 			});
+
+			// Don't delete yet - wait for success/error from subscriber
+			return;
 		}
+
+		// For forms that submit normally (not prevented)
+		// We can clean up the cache on successful submission
+		// This would typically be called from handleFormSuccess
+	}
+
+	handleFormSuccess(form, data) {
+		// Clear previous errors
+		form.querySelectorAll('.error-message').forEach(el => el.remove());
+		form.querySelectorAll('.field-error').forEach(el =>
+			el.classList.remove('field-error')
+		);
+
+		// Add success class to form
+		form.classList.add('form-success');
+
+		// Show success message if provided
+		if (data.message) {
+			const success = document.createElement('div');
+			success.className = 'form-success-message success-message';
+			success.textContent = data.message;
+			form.insertBefore(success, form.firstChild);
+
+			const icon = window.getIcon?.('check-circle');
+			if (icon) {
+				icon.classList.add('success-icon');
+				success.prepend(icon);
+			}
+		}
+
+		// If there's a title/description (for registration success)
+		if (data.title || data.description) {
+			const successBox = document.createElement('div');
+			successBox.className = 'success-box';
+
+			if (data.title) {
+				const title = document.createElement('h3');
+				title.textContent = data.title;
+				successBox.appendChild(title);
+			}
+
+			if (data.description) {
+				const descriptions = Array.isArray(data.description)
+					? data.description
+					: [data.description];
+
+				descriptions.forEach(desc => {
+					const p = document.createElement('p');
+					p.textContent = desc;
+					successBox.appendChild(p);
+				});
+			}
+
+			form.insertBefore(successBox, form.firstChild);
+		}
+
+		// ✅ DELETE CACHED FORM DATA ON SUCCESS
+		if (form.dataset.formId) {
+			this.store.delete(form.dataset.formId).catch(err => {
+				console.warn('Failed to clear form cache:', err);
+			});
+
+			// Clear form config dirty state
+			const formConfig = this.forms.get(form.dataset.formId);
+			if (formConfig) {
+				formConfig.isDirty = false;
+				formConfig.lastSaved = Date.now();
+				formConfig.data = {}; // Clear cached data
+			}
+		}
+
+		// Announce success for accessibility
+		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) {
+		// Clear all previous errors
+		form.querySelectorAll('.error-message').forEach(el => el.remove());
+		form.querySelectorAll('.field-error, .has-error').forEach(el => {
+			el.classList.remove('field-error', 'has-error');
+		});
+
+		// Clear validation states using existing method
+		form.querySelectorAll('.field').forEach(fieldWrapper => {
+			this.clearValidation(fieldWrapper);
+		});
+
+		// Handle field-specific errors
+		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');
+				error.prepend(icon);
+			}
+
+			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}`
+				: `Form error: ${data.message}`;
+			window.jvbA11y.announce(announcement);
+		}
+
+		// Trigger custom event
+		form.dispatchEvent(new CustomEvent('jvb-form-error', {
+			detail: data
+		}));
 	}
 
 	handleClick(e) {
@@ -746,15 +954,16 @@
 	}
 
 	handleChange(event) {
-		if (this.subscribers.size > 0) {
-			const target = event.target;
-			const form = target.form || target.closest('form');
+		if (event.target.closest('[data-ignore]')) {
+			return;
+		}
+		const target = event.target;
+		const form = target.form || target.closest('form');if (!form) return;
 
-			if (!form) return;
+		const formConfig = this.forms?.get(form.dataset.formId);
+		if (!formConfig) 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) {
@@ -764,10 +973,8 @@
 			}
 
 			// Schedule auto-save if enabled
-			if (formConfig.options.autoSave && !form.dataset.noautosave) {
-				const delay = this.getDelayForField(target);
-				this.scheduleSave(formConfig, delay);
-			}
+			const delay = this.getDelayForField(target);
+			this.scheduleSave(formConfig, delay);
 		}
 	}
 
@@ -780,6 +987,9 @@
 	}
 
 	handleBlur(e) {
+		if (e.target.closest('[data-ignore]')) {
+			return;
+		}
 		const target = e.target;
 		const form = target.form || target.closest('form');
 
@@ -801,7 +1011,7 @@
 				this.validateField(input, fieldWrapper);
 			}
 			const formConfig = this.forms?.get(form.dataset.formId);
-			if (formConfig && formConfig.options.autoSave && !form.dataset.noautosave) {
+			if (formConfig) {
 				// Shorter delay on blur
 				this.scheduleSave(formConfig, {
 					type: 'blur',
@@ -813,6 +1023,9 @@
 	}
 
 	handleInput(e) {
+		if (e.target.closest('[data-ignore]') || ! e.target.closest('form')) {
+			return;
+		}
 		const input = e.target.closest('input, textarea, select');
 		if (!input) return;
 
@@ -974,6 +1187,7 @@
 
 		// All validations passed
 		this.showSuccess(fieldWrapper);
+		this.notify('field-validated', input);
 		return true;
 	}
 
@@ -998,7 +1212,7 @@
 	/**
 	 * Show success state (green checkmark)
 	 */
-	showSuccess(fieldWrapper) {
+	showSuccess(fieldWrapper, textMessage = '') {
 		if (!fieldWrapper) return;
 
 		// Find validation elements (they might be in field-input-wrapper or field-content)
@@ -1024,8 +1238,13 @@
 
 		// Hide error message
 		if (message) {
-			message.hidden = true;
-			message.textContent = '';
+			if (textMessage === '') {
+				message.hidden = true;
+				message.textContent = '';
+			} else {
+				message.hidden = false;
+				message.textContent = textMessage;
+			}
 		}
 	}
 
@@ -1244,6 +1463,9 @@
 		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}`;
 
@@ -1268,13 +1490,21 @@
 		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(()=> {
+		}).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
@@ -1286,13 +1516,14 @@
 		this.forms.set(formConfig.id, formConfig);
 		document.removeEventListener('input', this.handleInput);
 
-		for (let [key,  value] of Object.entries(formData)) {
-			//We want all data for complex fields, like group, repeater, or location
+		for (let [key, value] of Object.entries(formData)) {
+			// Complex fields need full data
 			if (typeof value === 'object') {
 				changes[key] = value;
 			}
 		}
-		// Notify instead of callback
+
+		// Notify
 		this.notify('form-autosave', {
 			formId: formConfig.id,
 			changes: changes,
@@ -1313,16 +1544,23 @@
 
 		// Check if current data differs from snapshot
 		const currentData = this.collectFormData(formConfig.element);
-		const changes = this.getChangedFields(formConfig.lastSnapshot, currentData);
+		const changes = this.getChangedFields(formConfig.data, currentData);
 
 		return Object.keys(changes).length > 0;
 	}
 
-	showFormStatus(formID, status) {
+	showFormStatus(formID, status, message='') {
 		// Remove existing status
 		let form = this.forms.get(formID);
+		if (!form.options.formStatus) {
+			return;
+		}
 
-		console.log('Setting status: ', status);
+		if (form.status === status){
+			return;
+		}
+
+		form.status = status;
 
 		// Add new status
 		const statusWrap = form.element.querySelector('.fstatus');
@@ -1341,9 +1579,9 @@
 			'offline': 'Changes will be saved when online'
 		};
 		const icons = {
-			'autosaved': 'check',
-			'submitted': 'check',
-			'error': 'close',
+			'autosaved': 'check-circle',
+			'submitted': 'check-circle',
+			'error': 'close-circle',
 			'offline': 'cloud-slash',
 			'pending': 'exclamation-mark'
 		}
@@ -1352,9 +1590,10 @@
 		if (icon) {
 			statusWrap.prepend(icon);
 		}
-		console.log(status, messages[status]);
-		console.log(status, icons[status]);
-		statusElement.textContent = messages[status] || status;
+		if (message === '') {
+			message = messages[status] || status;
+		}
+		statusElement.textContent = message;
 		statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status));
 
 		// Auto-hide success messages
@@ -1381,7 +1620,15 @@
 
 	/* ========== Form Data Methods ========== */
 
-	collectFormData(form) {
+	collectFormData(form, isInit = false) {
+		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 = {};
@@ -1400,8 +1647,52 @@
 		return this.mergeRepeaterData(data, repeaterData);
 	}
 
+	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;
+	}
+
 	getFieldProcessor(key) {
-		if (key.includes('|')) return this.processTableField;
 		if (key.includes('::')) return this.processGroupField;
 		if (key.includes(':')) return this.processRepeaterField;
 		if (/\[[^\]]+\]/.test(key)) return this.processLocationField;
@@ -1426,42 +1717,12 @@
 	}
 
 	mergePostData(data, postData) {
-		for (let [postId, postData] in Object.entries(postData)) {
-			data[postId] = postData;
+		for (let [postId, fields] of Object.entries(postData)) {
+			data[postId] = fields;
 		}
 		return data;
 	}
 
-	processTableField(key, value, data, repeaterData, postData, form) {
-		/***
-		 * Table forms are a huge form containing multiple posts and their data
-		 * Field names are prepended with  `${postID}|`
-		 * Goal:
-		 * 1) Separate out the post id from the field name
-		 * 2) store the original data in a temporary 'original' variable
-		 * 3) Process the field as normal
-		 * 4) return the original data, as PostID: {$field data}
-		 * Final format:
-		 * {
-		 *     id1: {
-		 *         field1: "A title",
-		 *         field3: 32
-		 *     },
-		 *     id2: {
-		 *         field1: "Another title",
-		 *         field2: "122,21,32"
-		 *     }
-		 * }
-		 **/
-		let [post, fieldKey] = key.split('|');
-		if (!post in postData) {
-			postData[post] = {};
-		}
-
-		const processor = this.getFieldProcessor(fieldKey);
-		processor(fieldKey, value, postData, repeaterData, postData, form);
-
-	}
 	processRepeaterField(key, value, data, repeaterData, postData, form) {
 		let [fieldName, index, subField] = key.split(':');
 

--
Gitblit v1.10.0