From 56a9a1ccf764ff7a6af8f8a2292cb07443cb4aa7 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 28 May 2026 18:19:57 +0000
Subject: [PATCH] =New Gitbit setpu

---
 assets/js/concise/CRUD.js | 1528 +++++++++++++++++++++++++++++++++++++---------------------
 1 files changed, 966 insertions(+), 562 deletions(-)

diff --git a/assets/js/concise/CRUD.js b/assets/js/concise/CRUD.js
index 9e2e9bd..82b8b4c 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -10,15 +10,16 @@
 		this.queue = window.jvbQueue;
 		this.a11y = window.jvbA11y;
 		this.error = window.jvbError;
+		this.populate = window.jvbPopulate;
 		this.cache = new window.jvbCache(this.content);
 
 		this.activeItem = null;
 		this.isTimeline = false;
-		this.items = {
-			list: new Map(),
-			grid: new Map(),
-			table: new Map()
-		}; //DOM references
+		this.isPopulating = false;
+
+		//Track Changes
+		this.changes = new Map();
+		this.items = new Map();
 
 		this.init();
 	}
@@ -26,6 +27,7 @@
 	init() {
 		this.initElements();
 		this.initListeners();
+		this.defineTemplates();
 		let cached = this.initSettings();
 		this.initStore(cached);
 		this.checkHideFilters();
@@ -34,6 +36,221 @@
 		this.initModals();
 	}
 
+	defineTemplates() {
+		const T = window.jvbTemplates;
+		const crud = this;
+
+		const baseSetup = (el, refs, data) => {
+			el.dataset.itemId = data.id;
+			let wrapper = refs.checkbox.closest('.preview');
+			window.prefixInput(refs.checkbox, `select-${data.id}`, wrapper, true);
+			refs.checkbox.value = data.id;
+			refs.checkbox.checked = crud.selected.has(parseInt(data.id));
+			if (refs.selectLabel) refs.selectLabel.htmlFor = `select-${data.id}`;
+
+			if (refs.edit) refs.edit.dataset.id = data.id;
+			if (refs.trash) refs.trash.dataset.id = data.id;
+		};
+		const imageSetup = function(el, refs, data) {
+			let hasThumbnail = data?.fields?.post_thumbnail || data?.fields?.thumbnail;
+			if (hasThumbnail) {
+				const thumbnail = data.images[hasThumbnail] ?? {};
+				refs.img.src = thumbnail.medium??'';
+				refs.img.alt = thumbnail.alt??data.fields.post_title??'';
+			}
+
+		}
+
+		T.define('gridView', {
+			refs: {
+				img: 'img',
+				checkbox: '.select-item',
+				selectLabel: 'label.select-item-label',
+				edit: '[data-action="edit"]',
+				trash: '[data-action="trash"]'
+			},
+			setup({ el, refs, manyRefs, data }) {
+				baseSetup(el, refs, data);
+				imageSetup(el, refs, data);
+			}
+		});
+
+		T.define('listView', {
+			refs: {
+				img: 'img',
+				checkbox: '.select-item',
+				selectLabel: 'label.select-item-label',
+				edit: '[data-action="edit"]',
+				trash: '[data-action="trash"]'
+			},
+			manyRefs: {
+				attrs: '[data-attr]',
+				fields: '[data-field]'
+			},
+			setup({ el, refs, manyRefs, data }) {
+				baseSetup(el, refs, data);
+				imageSetup(el, refs, data);
+				manyRefs?.attrs?.forEach(el => {
+					const value = data[el.dataset.attr];
+					if (value && value !=='') {
+						el.textContent = value;
+					} else {
+						el.remove();
+					}
+				});
+				manyRefs?.fields?.forEach(el => {
+					const value = data.fields?.[el.dataset.field];
+					if (value && value !== '') {
+						el.tagName === 'DIV' ? el.innerHTML = value : el.textContent = value;
+					} else {
+						el.remove();
+					}
+				});
+			}
+		});
+
+		let tableRefs = {};
+		let tableMany = {};
+		if (this.isTimeline) {
+			tableRefs.sharedRow = 'tr.shared';
+			tableRefs.point = 'tr.timeline-point';
+		}
+		T.define('tableView', {
+			refs: {
+				checkbox: '.select-item',
+				selectLabel: 'label.select-item-label',
+				... tableRefs,
+			},
+			manyRefs: {
+				inputs: 'input,select,textarea',
+				status: 'input[name="post_status"]',
+				selectors: '[data-type="selector"]',
+				fields: '[data-field]',
+				... tableMany,
+			},
+			setup({ el, refs, manyRefs, data }) {
+				baseSetup(el, refs, data);
+
+				manyRefs?.inputs?.forEach(el => {
+					let wrapper = el.closest('[data-field]');
+					window.prefixInput(el, `${data.id}-`, wrapper);
+				});
+
+				manyRefs?.status?.forEach(el => {
+					if (el.value === data.status) {
+						el.checked = true;
+					}
+				});
+
+				if (crud.isTimeline) {
+					if (refs.sharedRow) {
+						refs.sharedRow.querySelectorAll('input,select,textarea').forEach(input => {
+							let wrapper = input.closest('[data-field]');
+							window.prefixInput(input, `${data.id}-`, wrapper);
+						});
+
+						crud.populate.populate(refs.sharedRow, data);
+
+						// Handle status radios in shared row
+						refs.sharedRow.querySelectorAll('input[name="post_status"]').forEach(el => {
+							if (el.value === data.status) {
+								el.checked = true;
+							}
+						});
+					}
+
+					if (refs.point && data.fields?.timeline) {
+
+						Object.entries(data.fields.timeline).forEach(([nuthing, timeline], index) => {
+							const point = refs.point.cloneNode(true);
+							point.dataset.index = `${index}`;
+							point.dataset.itemId = timeline.id;
+
+							point.querySelectorAll('input,select,textarea').forEach(input => {
+								let wrapper = input.closest('[data-field]');
+								window.prefixInput(input, `${timeline.id}-`, wrapper);
+							});
+
+							crud.populate.populate(point, {
+								fields: timeline,
+								images: data.images,
+								taxonomies: data.taxonomies
+							});
+
+							const imgData = data.images?.[timeline.post_thumbnail];
+							if (imgData) {
+								point.querySelector('.field.upload')?.setAttribute('title', imgData['image-title']??'');
+							}
+							el.insertBefore(point, refs.point);
+						});
+						refs.point.remove();
+					}
+				} else {
+					if (crud.ui.table.form?.dataset.edit !== undefined) {
+						// Non-timeline: prefix all inputs normally
+						manyRefs?.inputs?.forEach(input => {
+							let wrapper = input.closest('[data-field]');
+							window.prefixInput(input, `${data.id}-`, wrapper);
+						});
+
+						manyRefs?.status?.forEach(el => {
+							if (el.value === data.status) {
+								el.checked = true;
+							}
+						});
+
+						crud.populate.populate(el, data);
+					} else {
+						const fields = (Object.hasOwn(data, 'fields')) ? data.fields : data;
+						manyRefs?.fields?.forEach(field => {
+							if (Object.hasOwn(fields, field.dataset.field) && fields[field.dataset.field] !== '') {
+								let value = fields[field.dataset.field];
+								let p = fields.children[0];
+								if (p) {
+									p.textContent = field.dataset.field === 'date'
+										? window.formatTimeAgo(value)
+										: value;
+								}
+							}
+						});
+					}
+				}
+
+				manyRefs?.selectors?.forEach(selector => selector.setAttribute('data-lazy', ''));
+			}
+		});
+
+		T.define('emptyState');
+
+		T.define('bulkItem', {
+			refs: {
+				checkbox: 'input',
+				img: 'img',
+				label: 'label'
+			},
+			setup({el, refs, manyRefs, data}) {
+				if (refs.checkbox) {
+					refs.checkbox.id = `bulk_${data.id}`;
+					refs.checkbox.value = data.id;
+					refs.checkbox.checked = true;
+					refs.checkbox.name ='selected[]';
+				}
+				let thumbnail = data?.images[data?.fields?.post_thumnbail]??{};
+				if (refs.img && Object.keys(thumbnail).length >0) {
+					refs.img.src = thumbnail.medium??'';
+					refs.img.alt = thumbnail.alt??'';
+				}
+
+				if (refs.label) {
+					refs.label.title = item.fields.post_title;
+				}
+			}
+		});
+		T.define('trashOptions');
+		T.define('notTrashOptions');
+		T.define('contentTable');
+	}
+
 	initElements() {
 		this.allowedFilters = ['status', 'orderby', 'order', 'search', 'date-filter', 'dateFrom', 'dateTo'];
 		this.selectors = {
@@ -107,7 +324,11 @@
 				},
 				date: '[data-filter="date"]'
 			},
-			uploader: 'details.uploader'
+			uploader: {
+				details: 'details.uploader',
+				form: 'details.uploader form',
+				uploader: 'details.uploader [data-field-type="upload"]'
+			}
 		}
 
 		this.ui = window.uiFromSelectors(this.selectors);
@@ -123,17 +344,32 @@
 		this.isTimeline = !!document.querySelector('[data-timeline]');
 	}
 		initUploader() {
-			if (!this.ui.uploader) return;
+			if (!this.ui.uploader.form) return;
+			this.uploadForm = this.forms.registerForm(this.ui.uploader.form).id??false;
 
-			window.jvbUploads.scanFields(this.ui.uploader);
+			// window.jvbUploads.scanFields(this.ui.uploader);
 			window.jvbUploads.subscribe((event, data) => {
 				if (event === 'sent-to-queue') {
-					if (data === this.ui.uploader.dataset.uploader) {
+					if (data.field.id === this.ui.uploader.uploader.dataset.uploader) {
+						if (this.uploadForm ) {
+							this.forms.store.delete(this.uploadForm);
+						}
+
 						window.debouncer.schedule('crud-complete', ()=> {
 							this.store.clearCache();
 						});
 					}
 				}
+
+				if (event === 'sent-to-queue' && data.field) {
+					const fieldName = data.field.config.name;
+					const itemId = data.field.config.itemID;
+					if (itemId && fieldName) {
+						if (this.changes.has(itemId)) {
+							delete this.changes.get(itemId)[fieldName];
+						}
+					}
+				}
 			});
 		}
 		initModals() {
@@ -145,17 +381,22 @@
 				this.modals[name].subscribe((event, data) => {
 					switch (event) {
 						case 'modal-close':
-							this.activeItem = null;
 							const formId = this.ui.modals[name].form.dataset.formId;
 							if (formId) {
-								this.formController.cleanupForm(formId);
+								this.forms.clearForm(formId);
 							}
 
-							this.ui.modals[name].form.reset();
+							this.resetForm(this.ui.modals[name].form);
 
 							if (name === 'date') {
 								this.handleCustomDateSelection()
 							}
+							if (['edit','bulkEdit','create'].includes(name)) {
+								//handle escapes (not form submits)
+								if (window.debouncer.timeouts.has(`save-${this.content}`)) {
+									this.scheduleSave(0);
+								}
+							}
 							break;
 						case 'modal-open':
 
@@ -171,29 +412,40 @@
 			... this.defaults,
 			...cached
 		};
-		const store = window.jvbStore.register(
+
+
+		const stores = window.jvbStore.register(
 			this.content,
-			{
-				storeName: this.content,
-				keyPath: 'id',
-				endpoint: this.endpoint??'content',	//for taxonomy stores
-				headers: {
-					'action_nonce': window.auth.getNonce('dash'),
+		[
+				{
+					storeName: this.content,
+					keyPath: 'id',
+					endpoint: this.endpoint??'content',	//for taxonomy stores
+					headers: {
+						'X-Action-Nonce': window.auth.getNonce('dash'),
+					},
+					indexes: [
+						{name: 'id', keyPath: 'id'},
+						{ name: 'status', keyPath: 'status'},
+						{ name: 'date', keyPath: 'date'},
+						{ name: 'modified', keyPath: 'modified'},
+						{ name: 'title', keyPath: 'title'},
+					],
+					isAuth: true,
+					filters: filters,
+					ignore: ['content', 'user'],
+					TTL: 60 * 60 * 1000, 		//1 hour cache
+					showLoading: true,
 				},
-				indexes: [
-					{name: 'id', keyPath: 'id'},
-					{ name: 'status', keyPath: 'status'},
-					{ name: 'date', keyPath: 'date'},
-					{ name: 'modified', keyPath: 'modified'},
-					{ name: 'title', keyPath: 'title'}
-				],
-				filters: filters,
-				ignore: ['content', 'user'],
-				TTL: 60 * 60 * 1000, 		//1 hour cache
-				showLoading: true,
-			}
+				{
+					storeName: 'changes',
+					keyPath: 'id'
+				}
+			]
 		);
-		this.store = store[this.content];
+
+		this.changesStore = stores['changes'];
+		this.store = stores[this.content];
 
 		this.store.subscribe((event, data) => {
 			switch (event) {
@@ -202,18 +454,35 @@
 					this.selectionHandler.collectItems();
 					break;
 			}
-		})
+		});
+
+		this.changesStore.subscribe((event, data) => {
+			switch (event) {
+				case 'data-ready':
+					let changes = this.changesStore.getAll();
+					if (changes.length > 0) {
+						changes.forEach(change => {
+							this.changes.set(change.id, change);
+						});
+						this.savePosts('', false).then(()=>{});
+					}
+					break;
+			}
+		});
 	}
 	initIntegrations() {
 		this.selected = new Set();
 		this.selectionHandler = new window.jvbHandleSelection(this.container, {
 			selectAll: {
-				checkbox: '.crud #select-all',
+				checkbox: '#select-all',
 				label: '.bulk-select label',
 				span: '.bulk-select label span'
 			},
 			wrapper: {
 				wrapper: '.wrap'
+			},
+			item: {
+				idAttribute: 'itemId'
 			}
 		});
 		this.selectionHandler.subscribe((event, data) => {
@@ -223,16 +492,24 @@
 			this.ui.bulk.count.textContent = `${this.selected.size} ${this.plural} selected`;
 		});
 
-		this.formController = new window.jvbForm();
+		this.forms = window.jvbForm;
 
-		this.formController.subscribe((event, data) => {
-			switch(event) {
-				case 'form-submit':
-				case 'form-autosave':
-					this.handleFormChange(event,data);
-					break;
-			}
-		});
+		// this.forms.subscribe((event, data) => {
+		// 	switch(event) {
+		// 		case 'form-submit':
+		// 		case 'form-autosave':
+		// 			// this.handleFormChange(event,data);
+		// 			break;
+		// 	}
+		// });
+
+		if (window.jvbUploads) {
+			window.jvbUploads.subscribe((event, data) => {
+				if (event === 'groups_uploaded' && data.content === this.content) {
+					this.handleGroupsUploaded(data);
+				}
+			});
+		}
 
 		this.queue.subscribe((event, data) => {
 			if (['image_upload', 'video_upload', 'document_upload'].includes(data.type)
@@ -240,8 +517,92 @@
 				&& data.status === 'completed') {
 				this.store.clearCache();
 			}
+
+
+			if (event === 'operation-status'
+				&& data.status === 'completed'
+				&& data.endpoint === 'uploads/groups') {
+				if (data.result && data.result.group_mappings) {
+					console.log('Handling group mapping from queue response');
+					this.handleGroupMappings(data.result.group_mappings);
+				}
+
+				this.store.clearCache();
+			}
+
+			if (event === 'operation-status'
+				&& data.status === 'completed'
+				&& data.type === 'content_update') {
+
+				this.store.clearCache();
+
+				if (!data.result || !data.result.success || !data.result.errors)
+				{
+					console.warn('Content update completed but no results', data);
+					return;
+				}
+
+				if (Object.keys(data.result.success).length > 0) {
+					this.checkCompletedChanges(Object.entries(data.result.success));
+				}
+				if (Object.keys(data.result.errors).length > 0) {
+					this.checkFailedChanges(Object.entries(data.result.errors));
+					return;
+				}
+
+				if (Object.keys(data.result.success).length === 0) {
+					console.log(data.result.success);
+					data.result.success.forEach(id => this.changesStore.delete(id));
+
+					this.store.clearCache();
+				}
+			}
+
+			if (event === 'sent-to-server' && data.type === 'content_update') {
+				if (data instanceof FormData) return;
+
+				for ( let [id, changes] of Object.entries(data.posts)) {
+					this.compareStored(id, changes);
+				}
+			}
+
 		});
 	}
+	checkCompletedChanges(items) {
+		for (let [id, data] of items) {
+			this.compareStored(id, data);
+		}
+	}
+		compareStored(id, data) {
+			let stored = this.changesStore.get(id);
+			if (!stored) return;
+
+			for (let [field, value] of Object.entries(data)) {
+				if (Object.hasOwn(stored, field)) {
+					let changes = window.getDifferences.map(stored[field], value);
+					if (!changes) {
+						delete stored[field];
+					} else {
+						stored[field] = changes;
+					}
+				}
+			}
+
+			let hasID = Object.hasOwn(stored, 'id');
+			let hasContent = Object.hasOwn(stored, 'content');
+			if ((hasID && hasContent && Object.keys(stored).length === 2)
+				|| ((hasID || hasContent) && Object.keys(stored).length === 1)
+				|| Object.keys(stored).length === 0
+			) {
+				this.changesStore.delete(id);
+				this.store.clearCache();
+			} else {
+				this.changesStore.save(stored);
+			}
+		}
+	checkFailedChanges(items) {
+		//TODO do something.
+	}
 
 	initSettings() {
 		this.defaults = {
@@ -304,7 +665,7 @@
 				default: 'closed',
 			},
 			showUploader: {
-				element: this.ui.uploader,
+				element: this.ui.uploader.details,
 				default: 'open'
 			}
 		};
@@ -327,53 +688,123 @@
 		this.changeHandler = this.handleChange.bind(this);
 		this.clickHandler = this.handleClick.bind(this);
 		this.inputHandler = this.handleInput.bind(this);
+		this.submitHandler = this.handleModalSubmit.bind(this);
 
 		document.addEventListener('change', this.changeHandler);
 		document.addEventListener('click', this.clickHandler);
 		if (this.ui.filters.search) {
 			this.ui.filters.search.addEventListener('input', this.inputHandler);
 		}
+
+		for (let [name, modal] of Object.entries(this.ui.modals)) {
+			if (modal.form) {
+				modal.form.addEventListener('submit', this.submitHandler);
+			}
+		}
+	}
+
+	handleModalSubmit(e) {
+		e.preventDefault();
+		const form = e.target;
+		const modal = form.closest('dialog');
+		if (!modal) return;
+
+		if (modal.classList.contains('create')) {
+			this.handleCreateSubmit(modal);
+			return;
+		}
+
+		let title = `Saving changes for multiple ${this.plural}`;
+
+		this.scheduleSave(0);
+		this.modals.edit.handleClose();
+	}
+
+	async handleCreateSubmit(modal) {
+		const itemId = modal.dataset.itemId;
+
+		// 1. Flush changes to store
+		if (this.changes.size > 0) {
+			this.cancelBackup();
+			await this.handleBackup();
+		}
+
+		const changes = await this.changesStore.getAll();
+		if (changes.length === 0) return;
+
+		let allChanges = {};
+		changes.forEach(change => {
+			const { id, ...rest } = change;
+			allChanges[id] = rest;
+		});
+
+		// 2. Queue content creation, get operationId
+		let contentOpId = this.queue.addToQueue({
+			endpoint: this.endpoint,
+			headers: {
+				'X-Action-Nonce': window.auth.getNonce('dash'),
+			},
+			data: {
+				posts: allChanges,
+			},
+			popup: `Creating your new ${this.singular}`,
+			title: `Creating your new ${this.singular}`,
+		});
+
+		if (!contentOpId) return;
+
+		// 3. Queue any pending uploads with dependency on content creation
+		const uploadFields = modal.querySelectorAll('[data-upload-field]');
+		for (const fieldEl of uploadFields) {
+			const fieldId = fieldEl.dataset.uploader;
+			if (!fieldId) continue;
+
+			const uploads = window.jvbUploads.stores.uploads.filterByIndex({ field: fieldId });
+			if (uploads.length === 0) continue;
+
+			await window.jvbUploads.queueUploads('uploads', fieldId, contentOpId);
+		}
 	}
 	handleChange(e) {
-		const isSearch = window.targetCheck(e, this.selectors.filters.search);
-		if (isSearch) {
-			return;
-		}
-		const bulkAction = window.targetCheck(e, this.selectors.bulk.action);
-		if (bulkAction) {
-			this.handleBulkAction(bulkAction);
+		// Early bailout - target must be in an item or be a filter
+		const inItem = e.target.closest('[data-item-id]');
+		const isFilter = e.target.matches('[data-filter]');
+		const isBulkAction = e.target.matches('.bulk-action-select');
+		const isView = e.target.matches('[data-view]');
+
+		if (!inItem && !isFilter && !isBulkAction && !isView) return;
+
+		if (!this.isPopulating && inItem && !e.target.closest('[data-ignore], .select-item')) {
+			this.handleItemUpdate(e);
 			return;
 		}
 
-		let filter = window.targetCheck(e, '[data-filter]');
-		if (filter) {
-			this.handleFilterChange(filter);
+		if (isView) {
+			this.items.clear();
+			this.handleViewChange(e.target);
 			return;
 		}
 
-		let view = window.targetCheck(e, 'input[data-view]');
-		if (view) {
-			this.handleViewChange(view);
+		if (isBulkAction) {
+			this.handleBulkAction(e.target);
 			return;
 		}
 
+		if (isFilter) {
+			this.handleFilterChange(e.target);
+			return;
+		}
+
+		// Table-specific handlers
 		if (this.view === 'table') {
-			let target = window.targetCheck(e, '[data-id]');
-			if (target) {
-				this.handleTableChange(e);
-				return;
-			}
-
-			let multiSelect = window.targetCheck(e, 'details.multi-select');
-			if (multiSelect) {
+			if (e.target.matches('details.multi-select')) {
 				this.toggleColumn(e.target.id, e.target.checked);
 				return;
 			}
 
-			let tabNav = window.targetCheck(e, this.selectors.table.nav);
-			if (tabNav) {
-				this.tabNav = tabNav.checked;
-				this.cache.set('tabNav', tabNav.checked ? 'vertical' : 'horizontal');
+			if (e.target.matches(this.selectors.table.nav)) {
+				this.tabNav = e.target.checked;
+				this.cache.set('tabNav', e.target.checked ? 'vertical' : 'horizontal');
 			}
 		}
 	}
@@ -412,11 +843,8 @@
 	handleBulkTaxonomy(result) {
 		if (!result.termIds.length || !this.selected.size) return;
 
-		const changes = {};
-		const taxonomyField = `tax_${result.taxonomy}`;
-
 		this.selected.forEach(itemID => {
-			const item = this.store.get(parseInt(itemID));
+			const item = this.store.get(itemID);
 			if (!item) return;
 
 			// Merge existing terms with new ones
@@ -424,21 +852,117 @@
 			const existingIds = existingTerms.map(t => t.id);
 			const newIds = [...new Set([...existingIds, ...result.termIds])];
 
-			changes[itemID] = {
-				[taxonomyField]: newIds.join(','),
-				content: this.content
-			};
+			this.updateItem(itemID, result.taxonomy, newIds);
 		});
 
-		if (Object.keys(changes).length > 0) {
-			this.savePosts(
-				changes,
-				`Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`
-			);
-		}
+		this.savePosts(`Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`,).then(()=> {});
 
 		this.selectionHandler.clearSelection();
 	}
+
+	handleItemUpdate(e) {
+		let item = window.targetCheck(e, '[data-item-id]');
+		if (!item) return;
+
+		// Check if inside a collection field first
+		const collection = e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');
+
+		let name, value;
+		if (collection) {
+			name = collection.dataset.field;
+			value = this.forms.getFieldValue(collection);
+		} else {
+			let field = e.target.closest('[data-field]');
+			name = field.dataset.field;
+			value = this.forms.getFieldValue(e.target);
+		}
+
+		item.dataset.itemId.split(',').forEach(itemId => {
+			this.updateItem(itemId, name, value);
+		});
+	}
+	updateItem(itemId, name, value) {
+		if (this.isPopulating) {
+			return;
+		}
+		name.replace(`[${itemId}]`, '');
+
+		const stored = this.store.get(itemId);
+		if (stored) {
+			const storedValue = stored.fields?.[name] ?? stored[name];
+			const diff = window.getDifferences.map(storedValue, value);
+
+			if (diff === null) {
+				// Value matches stored — clean up any pending change for this field
+				if (this.changes.has(itemId)) {
+					delete this.changes.get(itemId)[name];
+					// If no real changes left, remove the item entirely
+					const remaining = Object.keys(this.changes.get(itemId))
+						.filter(k => k !== 'id' && k !== 'content');
+					if (remaining.length === 0) {
+						this.changes.delete(itemId);
+						this.changesStore.delete(itemId);
+					}
+				}
+				return;
+			}
+		}
+
+		if (!this.changes.has(itemId)) {
+			this.changes.set(itemId, { id: itemId, content: this.content });
+		}
+		this.changes.get(itemId)[name] = value;
+
+		this.scheduleBackup();
+		if (typeof itemId === 'number' || !String(itemId).includes('group')) {
+			this.scheduleSave();
+		}
+	}
+	scheduleBackup() {
+		window.debouncer.schedule(
+			`changes-${this.content}`,
+			async () => {
+				if (this.changes.size > 0) {
+					await this.handleBackup();
+				}
+			},
+			2000
+		);
+	}
+	cancelBackup() {
+		window.debouncer.cancel(`changes-${this.content}`);
+	}
+	async handleBackup() {
+		const changesArray = Array.from(this.changes.values());
+		this.changes.clear();
+
+		const ids = changesArray.map(c => c.id);
+		const existing = await Promise.all(
+			ids.map(id => this.changesStore.get(id))
+		);
+
+		const changes = changesArray.map((change, i) =>
+			existing[i] ? window.deepMerge(existing[i], change) : change
+		);
+
+		await this.changesStore.saveMany(changes);
+	}
+
+	scheduleSave(delay = 10000) {
+		window.debouncer.schedule(
+			`save-${this.content}`,
+			async () => {
+				// Ensure latest changes are in IndexedDB
+				if (this.changes.size > 0) {
+					this.cancelBackup();
+					await this.handleBackup();
+				}
+
+				await this.savePosts('', false);
+			},
+			delay
+		);
+	}
 	handleFilterChange(target) {
 		let filter = target.dataset.filter;
 
@@ -522,40 +1046,67 @@
 			this.cache.set('view', this.view);
 			this.render();
 		}
+
 	handleClick(e) {
-		let clearSearch = window.targetCheck(e, '.clear-search');
-		if (clearSearch) {
+		// Use matches() instead of closest() where possible (faster)
+		if (e.target.matches('.clear-search')) {
 			this.deleteFilter('search', '');
+			return;
 		}
-		let actionButton = window.targetCheck(e, '[data-action]');
+
+		const actionButton = e.target.closest('[data-action]');
 		if (actionButton) {
 			e.preventDefault();
-			let itemID = actionButton.dataset.id;
+			this.handleActionButton(actionButton);
+			return;
+		}
 
-			switch (actionButton.dataset.action) {
+		if (e.target.matches('.apply-date-filter')) {
+			this.handleCustomDateSelection();
+			this.modals.date.handleClose();
+			return;
+		}
+
+		if (e.target.matches(this.selectors.buttons.create) || e.target.closest(this.selectors.buttons.create)) {
+			this.openCreateModal();
+		}
+	}
+		openCreateModal(){
+			this.forms.registerForm(this.ui.modals.create.form,{
+				cache: false,
+			});
+			this.ui.modals.create.modal.dataset.itemId = window.generateID('new');
+
+			this.modals.create.handleOpen();
+		}
+		handleActionButton(button) {
+			const itemID = button.dataset.id;
+
+			switch (button.dataset.action) {
 				case 'edit':
 					this.openEditModal(itemID);
 					break;
 				case 'delete':
 					if (confirm('Delete this item? This cannot be undone')) {
-						let changes = {};
-						changes[itemID] = {
-							'post_status': 'delete',
-							'content': this.content
-						};
-						window.fade(actionButton.closest('.item'), false);
-						this.savePosts(changes, `Sending ${this.singular} to trash...`);
+						this.updateItem(itemID, 'post_status', 'delete');
+						window.fade(button.closest('.item'), false);
+						this.savePosts(`Permanently deleting ${this.singular}...`).then(()=>{});
 						this.store.delete(itemID);
 					}
 					break;
 				case 'trash':
-					let changes = {};
-					changes[itemID] = {
-						'post_status': 'trash',
-						'content': this.content
-					};
-					window.fade(actionButton.closest('.item'), false);
-					this.savePosts(changes, `Sending ${this.singular} to trash...`);
+					if (this.status === 'trash') {
+						if (confirm('Delete this item? This cannot be undone')) {
+							this.updateItem(itemID, 'post_status', 'delete');
+							window.fade(button.closest('.item'), false);
+							this.savePosts(`Permanently deleting ${this.singular}...`).then(()=>{});
+							this.store.delete(itemID);
+						}
+					} else {
+						this.updateItem(itemID, 'post_status', 'trash');
+						window.fade(button.closest('.item'), false);
+						this.savePosts(`Sending ${this.singular} to trash...`).then(()=>{});
+					}
 					break;
 				case 'bulk-edit':
 					if (this.selected.size > 0) {
@@ -563,10 +1114,7 @@
 					}
 					break;
 				case 'bulk-delete':
-					if (this.selected.size > 0 && confirm(`Delete  ${this.selected.size} items?`)) {
-						this.selected.forEach(id => this.store.delete(id));
-						this.selectionHandler.clearSelection();
-					}
+					this.handleBulkDelete();
 					break;
 				case 'refresh':
 					this.store.clearCache();
@@ -577,29 +1125,27 @@
 					break;
 			}
 		}
-
-		const applyDate = window.targetCheck(e, '.apply-date-filter');
-		if (applyDate) {
-			this.handleCustomDateSelection();
-			this.modals.date.handleClose();
-			return;
+		handleBulkDelete() {
+			let isTrash = this.status === 'trash';
+			if (this.selected.size > 0 && confirm(`${isTrash ? 'Permanently delete' : 'Send'} ${this.selected.size} ${this.selected.size === 1 ? this.singular : this.plural}${isTrash ? '' : 'to trash'}?`)) {
+				this.selected.forEach(id => {
+					this.store.delete(id);
+					this.updateItem(id, 'post_status', isTrash ? 'delete' : 'trash');
+				});
+				let title = isTrash
+					? `Permanently deleting ${this.selected.size} ${this.selected.size === 1 ? this.singular : this.plural}`
+					: `Sending ${this.selected.size} ${this.selected.size === 1 ? this.singular : this.plural} to trash`;
+				this.savePosts(title).then(()=>{});
+				this.selectionHandler.clearSelection();
+			}
 		}
 
-		const createButton = window.targetCheck(e, this.selectors.buttons.create);
-		if (createButton) {
-			this.ui.modals.create.form.reset();
-			this.formController.registerForm(this.ui.modals.create.form);
-			this.modals.create.handleOpen();
-		}
-	}
-
 	handleInput(e) {
 		e.preventDefault();
 		e.stopPropagation();
 		let query = e.target.value.trim();
 		let key = `${this.content}-search`;
 
-		console.log('Maybe search', query);
 		if (query.length === 0) {
 			this.deleteFilter('search', '');
 			return;
@@ -613,7 +1159,6 @@
 		window.debouncer.schedule(
 			key,
 			() => {
-				console.log('Searching for', query);
 				this.a11y.announce(`Searching for "${query}"...`);
 				this.store.setFilters({ search: query, page: 1 });
 			},
@@ -691,7 +1236,7 @@
 		}
 
 		// For regular tables, check for data-id
-		return !!row.dataset.id;
+		return !!row.dataset.itemId;
 	}
 
 	focusFieldInRow(row, fieldName, fromAbove = false) {
@@ -729,82 +1274,6 @@
 		return null;
 	}
 
-	toggleTimelineListeners(on = true) {
-		if (!this.isTimeline || this.view !== 'table') return;
-
-		// Cleanup existing instances
-		if (this.timelineSortables) {
-			this.timelineSortables.forEach(sortable => sortable.destroy());
-			this.timelineSortables = [];
-		}
-
-		if (!on) return;
-
-		// Initialize sortable for each tbody
-		this.timelineSortables = [];
-		const tbodies = this.ui.table.form.querySelectorAll('tbody.item');
-
-		tbodies.forEach(tbody => {
-			const sortable = new Sortable(tbody, {
-				animation: 150,
-				handle: '.drag-handle',
-				draggable: 'tr.timeline-point',
-				ghostClass: 'sortable-ghost',
-				chosenClass: 'sortable-chosen',
-				dragClass: 'sortable-drag',
-
-				// Prevent dragging between different tbodies
-				group: {
-					name: `timeline-${tbody.dataset.id}`,
-					pull: false,
-					put: false
-				},
-
-				onEnd: (evt) => this.handleTimelineReorder(evt)
-			});
-
-			this.timelineSortables.push(sortable);
-		});
-	}
-
-	handleTimelineReorder(evt) {
-		const { item: draggedRow, oldIndex, newIndex, from: tbody } = evt;
-
-		// No change
-		if (oldIndex === newIndex) return;
-
-		const itemID = parseInt(tbody.dataset.id);
-		const item = this.store.get(itemID);
-		if (!item?.fields?.timeline) return;
-
-		// Get current order of image IDs from DOM
-		const newOrder = Array.from(tbody.querySelectorAll('tr.timeline-point'))
-			.map(row => row.dataset.imageId)
-			.filter(Boolean);
-
-		// Rebuild timeline object in new order
-		const reorderedTimeline = {};
-		newOrder.forEach((imgId, index) => {
-			if (item.fields.timeline[imgId]) {
-				reorderedTimeline[imgId] = {
-					...item.fields.timeline[imgId],
-					order: index
-				};
-			}
-		});
-
-		item.fields.timeline = reorderedTimeline;
-
-		// Save to store and server
-		this.store.save(item);
-		this.savePosts(
-			{ [itemID]: { timeline: reorderedTimeline, content: this.content } },
-			'Reordering timeline...',
-			true
-		);
-
-		this.a11y?.announce(`Moved to position ${newIndex + 1}`);
-	}
 
 	/*******************************************************************
 	 MODALS
@@ -812,122 +1281,113 @@
 	openEditModal(itemID) {
 		let item = this.store.get(parseInt(itemID));
 		if (!item) return;
-		this.activeItem = itemID;
+		this.activeItem = item.id;
 		this.ui.modals.edit.modal.dataset.itemId = itemID;
 		this.ui.modals.edit.modal.dataset.content = this.content;
-		this.ui.modals.edit.h2.textContent = `Editing ${item.fields.post_title === '' ? this.singular : item.fields.post_title}`;
+		let title;
+		if (Object.hasOwn(item.fields, 'post_title')) {
+			title = item.fields.post_title;
+		} else if (Object.hasOwn(item.fields, 'name')) {
+			title = item.fields.name;
+		}
+		this.ui.modals.edit.h2.textContent = `Editing ${title === '' ? this.singular : title}`;
 		this.ui.modals.edit.form.dataset.formId = `edit-${itemID}`;
 
-		this.ui.modals.edit.form.reset();
-		new window.jvbPopulate(this.ui.modals.edit.form, item);
-		this.formController.registerForm(this.ui.modals.edit.form);
 
 		this.modals.edit.handleOpen();
+		this.forms.registerForm(this.ui.modals.edit.form, {cache: false,
+			autoUpload: true,});
+
+
+		this.isPopulating = true;
+		this.populate.populate(this.ui.modals.edit.form, item);
+		//For quill/taxonomy selector's async setups
+		requestAnimationFrame(() => {
+			requestAnimationFrame(() => {
+				this.isPopulating = false;
+			});
+		});
+
 	}
 	openBulkEditModal() {
 		window.removeChildren(this.ui.modals.bulkEdit.selected);
 		this.ui.modals.edit.form.reset();
-		this.modals.bulkEdit.handleOpen();
-		this.selected.forEach(itemId => {
-			let template = window.getTemplate('bulkItem');
-			if (!template) return;
-			let item = this.store.get(parseInt(itemId));
-			if (!item) return;
-			let [checkbox, img, label] = [template.querySelector('input'), template.querySelector('img'), template.querySelector('label')];
-			if (checkbox) {
-				checkbox.id = `bulk_${item.id}`;
-				checkbox.value = item.id;
-				checkbox.checked = true;
-				checkbox.name = 'selected[]';
-			}
 
-			let thumbnail = item.images[item.fields.post_thumbnail]??{};
-			if (img) {
-				img.src = thumbnail.medium??'';
-				img.alt = thumbnail.alt??''
-			}
+		window.chunkIt(
+			this.selected,
+			(itemId) => {
+				let item = this.store.get(parseInt(itemId));
+				if (!item) return;
+				itemIds.push(item.id);
 
-			label.title = item.fields['post_title']??'';
-			this.ui.modals.bulkEdit.selected.append(template);
-		});
+				return window.jvbTemplates.create('bulkItem', item);
+			},
+			(fragment) => this.ui.modals.bulkEdit.selected.append(fragment)
+		).then(()=>{});
+		let itemIds = Array.from(this.selected).map(id => this.store.get(parseInt(id))).filter(Boolean);
+
+		this.ui.modals.bulkEdit.modal.dataset.itemId = itemIds.join(',');
+
 		if (this.ui.modals.bulkEdit.h2) {
 			this.ui.modals.bulkEdit.h2.textContent = this.selected.size;
 		}
-		this.formController.registerForm(this.ui.modals.bulkEdit.form);
+		this.modals.bulkEdit.handleOpen();
+
+
+		this.forms.registerForm(this.ui.modals.bulkEdit.form, {cache:false});
+		this.isPopulating = true;
+		this.populate.populate(this.ui.modals.edit.form, item);
+		requestAnimationFrame(() => {
+			requestAnimationFrame(() => {
+				this.isPopulating = false;
+			});
+		});
 	}
 
 	/*****************************************************************
 	 FIELD HANDLING
 	*****************************************************************/
-	handleFormChange(event, data) {
-		let title = data.fullData.post_title;
-		let changes = (Object.hasOwn(data, 'changes')) ? data.changes : data.fullData;
 
-		let theChanges = {};
-		if (this.isTimeline) {
-			theChanges[this.activeItem] = changes;
-			if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
-				this.removeItems([this.activeItem]);
-			}
-			this.savePosts(theChanges, title, event === 'form-submit');
-			return;
+	async savePosts(title = '', delay = false) {
+		if (this.changes.size > 0) {
+			this.cancelBackup();
+			await this.handleBackup();
+		}
+		let changes = await this.changesStore.getAll();
+		if (changes.length === 0) return;
+
+		// Filter out false positives
+		changes = this.validateChanges(changes);
+		if (changes.length === 0) return;
+
+		if (title === '') {
+			title = `Saving ${changes.length} ${changes.length === 1 ? this.singular : this.plural}`;
 		}
 
+		let allChanges = {};
 		let remove = [];
-		let el = data.config.element;
-		if (el === this.ui.modals.edit.form) {
-			theChanges[this.activeItem] = changes;
-			title = `Saving ${title} Changes`;
-			if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
-				remove.push(this.activeItem);
+
+		changes.forEach(change => {
+			let itemId = change.id;
+			const { id, ...changeWithoutId } = change;
+			allChanges[itemId] = changeWithoutId;
+
+			if (change.post_status && this.shouldRemoveItemUI(change.post_status)) {
+				remove.push(itemId);
 			}
-		} else if (el === this.ui.modals.bulkEdit.form) {
-			let num = 0;
-			el.querySelectorAll('.selected input:checked').forEach(selected => {
-				theChanges[selected.value] = changes;
-				if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
-					remove.push(selected.value);
-				}
-				num++;
-			});
-			title = `Updating ${num} ${this.plural} Changes`;
-		} else if (el === this.ui.modals.create.form) {
-			if (event === 'form-submit') {
-				theChanges[el.dataset.formId] = changes;
-				title = `Saving ${title} Changes`;
-			}
-		}
+		});
+
 		if (remove.length > 0) {
 			this.removeItems(remove);
 		}
-		this.selectionHandler.clearSelection();
-		if (Object.keys(theChanges).length === 0) {
-			return;
-		}
-		this.savePosts(theChanges, title, event === 'form-submit');
-	}
-	savePosts(changes, title, delay = false) {
-		if (Object.keys(changes).length === 0) return;
-
-		let validChanges = {};
-		for (let itemId in changes) {
-			// Validate ID exists and is not null/undefined
-			if (!itemId || itemId === 'null' || itemId === 'undefined') {
-				console.warn('Skipping save for invalid ID:', itemId);
-				continue;
-			}
-			if (!changes[itemId]['content']) changes[itemId]['content'] = this.content;
-
-			validChanges[itemId] = changes[itemId];
-		}
 
 		let operation = {
 			endpoint: this.endpoint,
 			headers: {
-				'action_nonce': window.auth.getNonce('dash'),
+				'X-Action-Nonce': window.auth.getNonce('dash'),
 			},
 			data: {
-				posts: validChanges,
+				posts: allChanges,
 			},
 			delay: delay,
 			popup: `Saving changes`,
@@ -936,59 +1396,51 @@
 		this.queue.addToQueue(operation);
 	}
 
-	handleTableChange(e) {
-		const container = this.isTimeline
-			? e.target.closest('tbody[data-id]')
-			: e.target.closest('tr[data-id]');
-		if (!container) return;
+	/**
+	 * Compare pending changes against the store, removing unchanged fields.
+	 * Returns cleaned array (may be empty if nothing actually changed).
+	 */
+	validateChanges(changes) {
+		return changes.reduce((valid, change) => {
+			const { id, content, ...fields } = change;
+			const stored = this.store.get(id);
 
-		const input = e.target;
-		const field = input.closest('[data-field]')?.dataset.field;
-		if (!field) return;
-
-		const itemID = parseInt(container.dataset.id);
-		const item = this.store.get(itemID);
-		if (!item) return;
-
-		const value = this.getInputValue(input);
-
-		// Timeline-specific: check if it's a point or shared value
-		if (this.isTimeline) {
-			const timelinePoint = input.closest('tr.timeline-point');
-			if (timelinePoint) {
-				const imgID = timelinePoint.dataset.imageId;
-				item.fields.timeline ??= {};
-				item.fields.timeline[imgID] ??= {};
-				item.fields.timeline[imgID][field] = value;
-			} else {
-				item.fields[field] = value;
+			if (!stored) {
+				valid.push(change);
+				return valid;
 			}
-		} else {
-			item.fields[field] = value;
-		}
 
-		this.store.save(item);
-		this.savePosts({ [itemID]: item.fields }, `Saving changes...`, true);
+			const realChanges = { id, content };
+			let hasRealChange = false;
+
+			for (const [name, value] of Object.entries(fields)) {
+				const storedValue = stored.fields?.[name] ?? stored[name];
+				const diff = window.getDifferences.map(storedValue, value);
+
+				if (diff !== null) {
+					realChanges[name] = value;
+					hasRealChange = true;
+				}
+			}
+
+			if (hasRealChange) {
+				valid.push(realChanges);
+			} else {
+				this.changes.delete(id);
+				this.changesStore.delete(id);
+			}
+
+			return valid;
+		}, []);
 	}
 
-	getInputValue(input) {
-		if (input.type === 'checkbox') {
-			return input.checked ? (input.value || '1') : '';
-		}
-		if (input.type === 'radio') {
-			return input.checked ? input.value : null;
-		}
-		return input.value;
-	}
+
 	setBulkStatus(status) {
 		if (!['publish', 'draft', 'trash', 'delete'].includes(status)) return;
-
-		let changes = {};
+		let ids = [];
 		this.selected.forEach(itemID => {
-			changes[itemID] = {
-				post_status: status,
-				content: this.content
-			};
+			ids.push(itemID);
+			this.updateItem(itemID, 'post_status', status);
 		});
 		let title;
 		switch (status) {
@@ -999,12 +1451,12 @@
 				title = window.uppercaseFirst(status)+'ing';
 		}
 		if (this.shouldRemoveItemUI(status)) {
-			this.removeItems(Object.keys(changes));
+			this.removeItems(ids);
 		}
 		this.selectionHandler.clearSelection();
-		if (Object.keys(changes).length !== 0) {
-			this.savePosts(changes, `${title} ${Object.keys(changes).length} ${this.plural}...`);
-		}
+
+		this.savePosts(`${title} ${ids.length} ${ids.length === 1 ? this.singular : this.plural}...`).then(()=>{});
+
 	}
 	/***************************************************************
 	 VIEW
@@ -1021,7 +1473,7 @@
 				this.renderGrid(items);
 				break;
 			case 'table':
-				this.renderTable(items);
+				this.renderTable(items).then(()=>{});
 				break;
 			case 'list':
 				this.renderList(items);
@@ -1032,13 +1484,15 @@
 	updateUI() {
 		if (this.ui.bulk.action) {
 			let options = false;
-			let hasEdit =this.ui.bulk.action.querySelector('[value="edit"]');
-			if (this.status === 'trash' && hasEdit) {
+			let hasEdit = this.ui.bulk.action.querySelector('[value="edit"]');
+			let currentStatus = this.status;
+
+			if (currentStatus === 'trash' && hasEdit) {
 				window.removeChildren(this.ui.bulk.action);
-				options = window.getTemplate('trashOptions');
-			} else if (this.status !== 'trash' && !hasEdit) {
+				options = window.jvbTemplates.create('trashOptions');
+			} else if (currentStatus !== 'trash' && !hasEdit) {
 				window.removeChildren(this.ui.bulk.action);
-				options = window.getTemplate('notTrashOptions');
+				options = window.jvbTemplates.create('notTrashOptions');
 			}
 			if (options) {
 				options.querySelectorAll('option').forEach((option, index)=> {
@@ -1056,7 +1510,7 @@
 	renderEmpty() {
 		this.toggleTable(false);
 		window.removeChildren(this.ui.grid);
-		const empty = window.getTemplate('emptyState');
+		const empty = window.jvbTemplates.create('emptyState');
 		if (empty) {
 			this.ui.grid.append(empty);
 			this.a11y.announceItems(0,false,false);
@@ -1067,7 +1521,7 @@
 		if (this.ui.table.selectedColumns) this.ui.table.selectedColumns.hidden = !on;
 
 		if (on && !this.ui.table.form) {
-			let table = window.getTemplate('contentTable');
+			let table = window.jvbTemplates.create('contentTable');
 			this.container.append(table);
 			this.ui.table = window.uiFromSelectors(this.selectors.table);
 			this.ui.table.columns = this.container.querySelectorAll(this.selectors.table.columns);
@@ -1075,14 +1529,8 @@
 
 		if (this.ui.table.form) {
 			this.ui.table.form.hidden = !on;
-			if (on) {
-				this.formController.registerForm(this.ui.table.form, {
-					autosave: false,
-					formStatus: false,
-					isTable: true
-				});
-			} else {
-				this.formController.cleanupForm(this.ui.table.form.dataset.formId)
+			if (!on){
+				this.forms.clearForm(this.ui.table.form.dataset.formId)
 			}
 			if (this.ui.table.body) {
 				window.removeChildren(this.ui.table.body);
@@ -1094,7 +1542,7 @@
 		} else {
 			document.removeEventListener('keydown', this.keyHandler);
 		}
-		if (this.isTimeline && !on) this.toggleTimelineListeners(on);
+
 	}
 
 	renderGrid(items) {
@@ -1104,12 +1552,11 @@
 		this.ui.grid.classList.remove('list-view');
 		this.ui.grid.classList.add('grid-view');
 
-		const fragment = document.createDocumentFragment();
-		items.forEach(item => {
-			let card = this.renderGridItem(item);
-			fragment.appendChild(card);
-		});
-		this.ui.grid.appendChild(fragment);
+		window.chunkIt(
+			items,
+			(item) => this.renderGridItem(item),
+			(fragment) => this.ui.grid.append(fragment)
+		).then(()=>{});
 	}
 
 	renderList(items) {
@@ -1118,227 +1565,153 @@
 
 		this.ui.grid.classList.remove('grid-view');
 		this.ui.grid.classList.add('list-view');
-		const fragment = document.createDocumentFragment();
-		items.forEach(item => {
-			let row = this.renderListItem(item);
-			fragment.append(row);
-		});
-		this.ui.grid.append(fragment);
+		window.chunkIt(
+			items,
+			(item) => this.renderListItem(item),
+			(fragment) => this.ui.grid.append(fragment)
+		).then(()=>{});
 	}
 
-	renderTable(items) {
+	async renderTable(items) {
 		this.toggleTable();
 		window.removeChildren(this.ui.grid);
 
-		let fragment = document.createDocumentFragment();
-		items.forEach(item => {
-			let row = this.renderTableItem(item);
-			if (row) fragment.append(row);
+		await window.chunkIt(
+			items,
+			(item) => this.renderTableItem(item),
+			(fragment) => {
+				if (this.ui.table.body) {
+					this.ui.table.body.append(fragment);
+				} else {
+					this.ui.table.table.insertBefore(fragment, this.ui.table.foot);
+				}
+			},
+			5
+		);
+
+		requestAnimationFrame(() => {
+			window.jvbSelector?.scanExistingFields(this.ui.table.table);
 		});
-
-		if (this.ui.table.body) {
-			this.ui.table.body.append(fragment);
-		} else {
-			this.ui.table.table.insertBefore(fragment, this.ui.table.foot);
-		}
-
-		if (this.isTimeline) {
-			this.toggleTimelineListeners(true);
-		}
 	}
 
 	/***************************************************************
  	RENDER HELPERS
 	 ***************************************************************/
-	setupItemElement(element, item, templateName) {
-		if (this.items[this.view].has(item.id)) {
-			return this.items[this.view].get(item.id);
-		}
-
-		element.dataset.id = item.id;
-		if (item._pending) element.classList.add('pending');
-
-		// Setup selection checkbox/label
-		this.setupItemSelection(element, item);
-
-		// Setup thumbnail
-		this.setupItemThumbnail(element, item);
-
-		// Setup action buttons
-		this.setupItemActions(element, item);
-
-		this.items[this.view].set(item.id, element);
-		return element;
-	}
-
-	setupItemSelection(element, item) {
-		const checkbox = element.querySelector('.select-item');
-		const label = element.querySelector('.select-item + label, .select-item-label');
-		if (!checkbox) return;
-
-		checkbox.id = `select-${item.id}`;
-		checkbox.value = item.id;
-		checkbox.checked = this.selected.has(parseInt(item.id));
-
-		if (label) {
-			label.htmlFor = `select-${item.id}`;
-		}
-	}
-
-	setupItemThumbnail(element, item) {
-		const img = element.querySelector('img');
-		if (!img) return;
-
-		const thumbnail = item.images?.[item.fields?.post_thumbnail] ?? {};
-		img.src = thumbnail.medium ?? '';
-		img.alt = thumbnail.alt ?? '';
-	}
-
-	setupItemActions(element, item) {
-		element.querySelectorAll('[data-action]').forEach(btn => {
-			btn.dataset.id = item.id;
-		});
-	}
-
 	renderGridItem(item) {
-		if (this.items.grid.has(item.id)) {
-			return this.items.grid.get(item.id);
-		}
-		const card = window.getTemplate('gridView');
-		if (!card) return null;
-		return this.setupItemElement(card, item, 'grid');
+		let gridItem = window.jvbTemplates.create('gridView', item);
+		this.items.set(item.id, gridItem);
+		return gridItem;
 	}
 
 	renderListItem(item) {
-		if (this.items.list.has(item.id)) {
-			return this.items.list.get(item.id);
-		}
-		let row = window.getTemplate('listView');
-		if (!row) return null;
-
-		row = this.setupItemElement(row, item, 'list');
-
-		// List-specific: populate data attributes
-		row.querySelectorAll('[data-attr]').forEach(el => {
-			const value = item[el.dataset.attr];
-			if (value && value !== '') {
-				el.textContent = value;
-			} else {
-				el.remove();
-			}
-		});
-
-		row.querySelectorAll('[data-field]').forEach(el => {
-			const value = item.fields?.[el.dataset.field];
-			if (value && value !== '') {
-				el.tagName === 'DIV' ? el.innerHTML = value : el.textContent = value;
-			} else {
-				el.remove();
-			}
-		});
-		this.items.list.set(item.id, row);
-
-		return row;
+		let listItem = window.jvbTemplates.create('listView', item);
+		this.items.set(item.id, listItem);
+		return listItem;
 	}
 
 	renderTableItem(item) {
-		if (this.items.table.has(item.id)) {
-			return this.items.table.get(item.id);
-		}
-		let row = window.getTemplate('tableView');
-		if (!row) return null;
-
-		row = this.setupItemElement(row, item, 'table');
-
-		const status = row.querySelector(`input[name="post_status"][value="${item.status}"]`);
-		if (status) status.checked = true;
-
-		if (this.isTimeline) {
-			// Timeline: populate shared row, clone points
-			const sharedRow = row.querySelector('tr.shared');
-			if (sharedRow) {
-				new window.jvbPopulate(sharedRow, item);
-				this.cleanupTableRow(sharedRow);
-			}
-
-			const pointTemplate = row.querySelector('tr.timeline-point');
-			if (pointTemplate && item.fields?.timeline) {
-				Object.entries(item.fields.timeline).forEach(([imgId, timeline], index) => {
-					const point = pointTemplate.cloneNode(true);
-					point.dataset.index = index;
-					point.dataset.imageId = imgId;
-
-					new window.jvbPopulate(point, {
-						fields: timeline,
-						images: item.images,
-						taxonomies: item.taxonomies
-					});
-					this.cleanupTableRow(point);
-
-					const imgData = item.images?.[timeline.post_thumbnail];
-					if (imgData) {
-						point.querySelector('.field.upload')?.setAttribute('title', imgData['image-title'] || '');
-					}
-
-					row.insertBefore(point, pointTemplate);
-				});
-				pointTemplate.remove();
-			}
-		} else {
-			// Standard table row
-			if (this.ui.table.form?.dataset.edit !== undefined) {
-				new window.jvbPopulate(row, item);
-			} else {
-				this.populateTableReadOnly(row, item);
-			}
-			this.cleanupTableRow(row);
-		}
-
-		this.items.table.set(item.id, row);
-
-		return row;
+		let tableItem = window.jvbTemplates.create('tableView', item);
+		this.items.set(item.id, tableItem);
+		return tableItem;
 	}
 
-	populateTableReadOnly(row, item) {
-		for (const [key, value] of Object.entries(item)) {
-			const col = row.querySelector(`[data-field="${key}"]`);
-			if (!col) continue;
-
-			const p = col.querySelector('p');
-			if (p) {
-				p.textContent = col.dataset.fieldType === 'date'
-					? window.formatTimeAgo(value)
-					: value;
-			}
-		}
-	}
-
-	cleanupTableRow(row) {
-		row.querySelectorAll('td[data-field]').forEach(field => {
-			field.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach(label => {
-				if (!label.closest('.radio-options')) {
-					label.remove();
-				}
-			});
-
-			//Remove toggle labels for true_false fields
-			if (field.dataset.fieldType === 'true_false') {
-				field.querySelector('.toggle-label')?.remove();
-			}
-
-			//Remove field label for checkbox/radio groups
-			if (['checkbox','radio','select'].includes(field.dataset.fieldType)) {
-				field.querySelector('.label')?.remove();
-			}
-
-		});
-	}
 	toggleColumn(column, show) {
 		this.ui.table.table.querySelectorAll(`.${column}`).forEach(el =>{
 			el.hidden = !show;
 		});
 	}
 	/***************************************************************
+	 UPLOAD GROUP SUPPORT
+	 Handles:
+	  	- immediate UI feedback once the uploaded groups are sent to server
+	***************************************************************/
+	handleGroupsUploaded(data) {
+		const { posts, fieldId } = data;
+		let uploader = window.jvbUploads;
+		let field = uploader.fields.get(fieldId);
+
+		let added = [];
+		posts.forEach(post => {
+			const placeholderPost = {
+				id: post.groupId,
+				title: post.fields.post_title || `New ${this.singular}`,
+				status: 'draft',
+				date: new Date().toISOString(),
+				modified: new Date().toISOString(),
+				thumbnail: null,
+				icon: this.content,
+				taxonomies: {},
+				fields: post.fields,
+				images: {},
+			};
+
+			post.images.forEach((uploadId, index) => {
+				let id = uploadId['upload_id'];
+				if (index === 0) {
+					placeholderPost.fields['post_thumbnail'] = uploadId;
+				}
+				let upload = uploader.stores.uploads.get(id);
+				if (upload) {
+					placeholderPost.images[id] = {
+						'image-alt-text': '',
+						'image-caption': '',
+						'image-title': upload.fields.originalName,
+						medium: uploader.createPreviewUrl(uploader.formatFile(upload))
+					};
+				}
+
+			});
+			//
+			// // Add to store (won't persist since it's a fake ID)
+			// this.store.data.set(post.groupId, placeholderPost);
+			//
+			//
+			// // Render immediately
+			// let element;
+			// switch (this.view) {
+			// 	case 'grid':
+			// 		element = this.renderGridItem(placeholderPost);
+			// 		this.ui.grid.prepend(element);
+			// 		break;
+			// 	case 'list':
+			// 		element = this.renderListItem(placeholderPost);
+			// 		this.ui.grid.prepend(element);
+			// 		break;
+			// 	case 'table':
+			// 		element = this.renderTableItem(placeholderPost);
+			// 		if (this.ui.table.body) {
+			// 			this.ui.table.body.prepend(element);
+			// 		}
+			// 		break;
+			// }
+			// element.classList.add('uploading');
+			added.push(placeholderPost);
+		});
+		this.store.saveMany(added).then(() => this.render());
+
+
+		this.a11y.announce(`${posts.length} ${posts.length === 1 ? this.singular : this.plural} created. Waiting for server confirmation...`);
+	}
+
+	handleGroupMappings(mappings) {
+		// mappings = { "group_abc123": 456, "group_def456": 789 }
+
+		for (const [groupId, postId] of Object.entries(mappings)) {
+			// Get any pending changes for this temp item
+			let changes = {};
+			if (this.changes.has(groupId)) {
+				changes = this.changes.get(groupId);
+				this.changes.delete(groupId);
+			}
+			let storedChanges = this.changesStore.get(groupId)??{};
+			if (changes.size > 0 || storedChanges.size > 0) {
+				changes = window.deepMerge(storedChanges, changes);
+				this.changes.set(postId, changes);
+				this.scheduleBackup();
+			}
+		}
+	}
+	/***************************************************************
 	 UTILITY
 	***************************************************************/
 	shouldRemoveItemUI(newStatus) {
@@ -1346,15 +1719,11 @@
 			|| newStatus !== this.store.filters.status;
 	}
 	removeItems(items) {
-		let delay = 0;
 		items.forEach(itemId => {
-			setTimeout(() => {
-				let item = this.items[this.view].get(itemId);
-				if (item) {
-					window.fade(item, false);
-				}
-			}, delay);
-			delay += 50;
+			if (this.items.has(itemId)) {
+				let item = this.items.get(itemId);
+				if (item) window.fade(item, false);
+			}
 		});
 	}
 
@@ -1374,9 +1743,13 @@
 	setFilter(name, value) {
 		if (!this.allowedFilters.includes(name)) return;
 		this.cache.set(name, value);
+
+		if (name === 'status') this.status = value;
+		if (name === 'orderby') this.orderby = value;
+		if (name === 'order') this.order = value;
+
 		let el = this.findFilterEl(name, value);
 		this.setElValue(el, value);
-		//TODO: If we set the element to checked, does that automatically call the change listener, which then also sets the store filter and cache?
 		this.store.setFilter(name, value);
 	}
 
@@ -1439,8 +1812,6 @@
 		let el = this.ui.filters[name];
 		if (typeof el === 'object') {
 			if (!Object.hasOwn(this.ui.filters[name], value)) {
-				console.log('Sub filter el not found: ', value);
-				console.log(name);
 				return false;
 			}
 			el = this.ui.filters[name][value];
@@ -1450,19 +1821,52 @@
 	/***************************************************************
 	 CLEANUP
 	***************************************************************/
+	resetForm(form) {
+		// Clear text inputs, textareas
+		form.querySelectorAll('input[type="hidden"], input[type="text"], input[type="number"], input[type="email"], input[type="url"], textarea').forEach(input => {
+			input.value = '';
+		});
+
+		// Uncheck checkboxes and radios
+		form.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(input => {
+			input.checked = false;
+		});
+
+		// Reset selects to first option
+		form.querySelectorAll('select').forEach(select => {
+			select.selectedIndex = 0;
+		});
+
+		// Clear any selected items displays
+		form.querySelectorAll('.selected-items').forEach(container => {
+			window.removeChildren(container);
+		});
+
+		// Clear upload previews
+		form.querySelectorAll('.item-grid.preview').forEach(grid => {
+			window.removeChildren(grid);
+		});
+	}
 	destroy() {
+		window.debouncer.cancel(`changes-${this.content}`);
+		if (this.changes.size > 0) {
+			this.changesStore.saveMany(this.changes).then(()=>{});
+			this.changes.clear();
+		}
 		if (this.timelineSortables) {
 			this.timelineSortables.forEach(sortable => sortable.destroy());
 			this.timelineSortables = [];
 		}
+		for (let [name, modal] of Object.entries(this.ui.modals)) {
+			if (modal.form) {
+				modal.form.removeEventListener('submit', this.submitHandler);
+			}
+		}
 		document.removeEventListener('click', this.clickHandler);
 		document.removeEventListener('change', this.changeHandler);
 		if (this.ui.filters.search) {
 			this.ui.filters.search.removeEventListener('input', this.handleInput);
 		}
-		for (let map of Object.values(this.items)) {
-			map.clear();
-		}
 	}
 }
 

--
Gitblit v1.10.0