From ba1e1ccf869b818f7a7a897264dfea05563a7796 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 07 Jun 2026 20:10:20 +0000
Subject: [PATCH] =Major overhaul of Integrations. Playing around with adding fields to post types through Registrar from an integrations' class file.

---
 src/feed/view.js | 1185 ++++++++++++++++++++++++++++-------------------------------
 1 files changed, 564 insertions(+), 621 deletions(-)

diff --git a/src/feed/view.js b/src/feed/view.js
index 2462be7..a8c9946 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -1,203 +1,461 @@
 class FeedBlock {
 	constructor() {
 		this.container = document.querySelector('section.feed-block');
-		if (!this.container) {
-			return;
-		}
+		if(!this.container) return;
 
 		this.a11y = window.jvbA11y;
-		this.cache = new window.jvbCache('feed');
 		this.error = window.jvbError;
+		this.cache = new window.jvbCache('feed');
+		this.templates = window.jvbTemplates;
+		this.isFirstLoad = true;
 
 		this.config = {
-			source: '',
+			contextId: '',
 			context: '',
 			highlight: null,
 			gallery: false,
 			view: this.cache.get('feedView') || 'grid',
 			... this.container.dataset
 		};
+
+
+		this.init();
+	}
+	init() {
 		this.initElements();
+		this.defineTemplates();
+
+		this.initListeners();
 		this.initFilters();
 
+		this.initStore();
+		this.initTaxonomies().then(r => {});
 
-		this.loadWhenAble();
-	}
-
-	loadWhenAble() {
-		if ('requestIdleCallback' in window) {
-			requestIdleCallback(() => {
-				this.initTaxonomies();
-				this.initStore();
-				this.initListeners();
-				this.initGallery();
-			}, { timeout: 2000 });
-		} else {
-			setTimeout(() => {
-				this.initTaxonomies();
-				this.initStore();
-				this.initListeners();
-				this.initGallery();
-			}, 100);
-		}
+		this.processCachedFilters();
+		this.processURLFilters();
+		this.updateFilterUI();
+		this.initGallery();
 	}
 
 	initElements() {
-		this.currentTaxonomies = new Set();			// Allowed Taxonomies, grabbed from active buttons
-		this.taxonomyFilters = {};
-		this.elements = {
+		this.selectors = {
 			filterTrigger: '[data-filter]',
 			filters: {
-				content: '[data-filter="content"]',
-				orderby: '[data-filter="orderby"]',
-				order: '[data-filter="order"]',
-				match: '[data-filter="match"]',
+				actions: 	'.filter-actions .toggle-text',
+				container:	'.all-filters',
+				showing:	'.all-filters summary .current',
+				content: 	'[data-filter="content"]',
+				ordering:	'.ordering',
+				orderby: 	'[data-filter="orderby"]',
+				order: 		'[data-filter="order"]',
+				orderWrap:  '.order-direction',
+				match: 		'[data-filter="match"]',
 				favourites: '[data-filter="favourites"]',
-				taxonomy: '[data-filter^="taxonomy"]'
+				taxonomy: 	'[data-filter^="taxonomy"]',
 			},
-			selectedTax: '.selected-items',
-			clearFilter: 'button.clear-filters',
-			loadMore: 'button.load-more',
-			filterContainer: '.filters',
-			grid: '.item-grid',
+			grid: 		'.item-grid',
+			selected: 	'.selected-items',
+			buttons: {
+				loadMore: 		'button.load-more',
+				remove:			'.remove-term',
+				clearFilters: 	'button.clear-filters',
+				refresh:		'button[data-action="refresh"]'
+			}
 		};
-		this.ui = window.uiFromSelectors(this.elements);
+		this.ui = window.uiFromSelectors(this.selectors, this.container);
+		this.ui.buttons.refresh = document.querySelector(this.selectors.buttons.refresh);
 
-
-		this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
-		this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
-		if (this.ui.content && this.ui.content.length > 0) {
-			this.contentTypes = Array.from(
-				this.ui.content
-			).map(content => content.value);
-		} else {
-			this.contentTypes = [this.container.dataset['content']];
-		}
-
-		if (this.ui.taxonomies.length>0) {
-			this.taxonomies = Array.from(
-				this.ui.taxonomies,
-			).map(content => content.dataset.taxonomy);
-		} else {
-			this.taxonomies = [];
-		}
-
-
-	}
-
-	async initTaxonomies() {
-		this.selector = window.jvbSelector;
-		const buttons = document.querySelectorAll('[data-filter="taxonomy"]');
-
-		this.selector.isInitializing = true;
-		buttons.forEach((button) => {
-			const taxonomy = button.dataset.taxonomy;
-			this.currentTaxonomies.add(taxonomy);
-
-			this.selector.registerFilterButton(button, {
-				button: button,
-				buttonSelector: '[data-filter="taxonomy"]',
-				selected: this.ui.selectedTax
-			});
-
-			// Add preload listeners
-			this.addTaxonomyPreloadListeners(button, taxonomy);
+		//Add content and taxonomies
+		let getAll = ['content','orderby','order','taxonomy'];
+		getAll.forEach(item => {
+			let items = this.ui.filters.container.querySelectorAll(this.selectors.filters[item]);
+			this.ui[item] = Array.from(items);
 		});
 
-		this.selector.isInitializing = false;
-
-		this.selector.subscribe((event, data) => {
-			if (event === 'selected-terms') this.handleTaxonomyChange(data);
-		});
+		this.contentTypes = (this.ui.content.length > 0)
+			? this.ui.content.map(c => c.value)
+			: [this.container.dataset.content];
+		this.taxonomies = (this.ui.taxonomies?.length > 0)
+			? Array.from(this.ui.taxonomies).map(t => t.dataset.taxonomy)
+			: [];
 	}
 
-	addTaxonomyPreloadListeners(button, taxonomy) {
-		const preload = () => {
-			this.selector.preloadTaxonomy(taxonomy);
-		};
-
-		// Desktop hover
-		button.addEventListener('mouseenter', preload, { once: true });
-
-		// Touch/keyboard (fires before click)
-		button.addEventListener('pointerdown', preload, { once: true });
-
-		// Keyboard focus
-		button.addEventListener('focus', preload, { once: true });
-	}
-
-	handleTaxonomyChange(data) {
-		const { terms, taxonomy } = data;
-
-		// Update only the current taxonomy's terms
-		if (terms.size > 0) {
-			this.taxonomyFilters[taxonomy] = Array.from(terms.keys());
-		} else {
-			// Remove taxonomy if no terms selected
-			delete this.taxonomyFilters[taxonomy];
+	/**
+	 *
+	 * @param {string} item
+	 */
+	getChecked(item) {
+		if (!['content', 'orderby','order'].includes(item)) {
+			console.log('Invalid item to check: ', item);
 		}
 
-		// Build filters object with all taxonomies
-		let filters = {
-			page: 1
-		};
-
-		// Add taxonomy filters if any exist
-		if (Object.keys(this.taxonomyFilters).length > 0) {
-			filters.taxonomy = this.taxonomyFilters;
+		let items = this.ui[item];
+		if (!items) {
+			return;
 		}
 
-		this.updateFilter(filters);
+		let checked = items.filter(i => i.checked);
+		if (item === 'content' && checked.length > 0) {
+			this.updateContentFor(checked[0].value);
+		}
+		return checked.length === 0 ? items[0].value : checked[0].value;
 	}
 
-	clearAllTaxonomies() {
-		this.taxonomyFilters = {};
-		window.removeChildren(this.ui.selectedTax);
+	initListeners() {
+		this.popStateHandler	= this.handlePopState.bind(this);
+		this.clickHandler		= this.handleClick.bind(this);
+		this.changeHandler		= this.handleChange.bind(this);
 
-		this.updateFilter({
-			taxonomy: null,
-			page: 1
-		});
+		window.addEventListener('popstate', this.popStateHandler);
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
 	}
 
 	initFilters() {
-		//defaults
-		this.filters = {
-			content: 	this.contentTypes[0],
-			orderby: 	'date',
-			order: 		'desc',
-			page: 		1
+		this.allowedFilters = ['content', 'order', 'orderby', 'favourites', 'match'];
+		let defaults = {
+			content:	this.getChecked('content'),
+			orderby:	this.getChecked('orderby'),
+			order:		this.getChecked('order'),
+			page:		1,
 		};
-		if (this.config.context) this.filters.context = this.config.context;
-		if (this.config.source) this.filters.source = this.config.source;
+		if (this.config.context) defaults.context = this.config.context;
+		if (this.config.contextId) defaults.contextId = this.config.contextId;
 
-		//check the cache
-		this.processCachedFilters();
-		//check url
-		this.processURLFilters();
+		this.filters = defaults;
 
-		// Set initial UI state
-		this.syncUIToFilters();
+		this.defaults = {...defaults};
 	}
-	syncUIToFilters() {
-		if (this.ui.filterContainer) {
-			// Check radio buttons
-			Object.entries(this.filters).forEach(([key, value]) => {
-				const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
-				if (input) {
-					input.checked = true;
+	updateFilterUI() {
+		if (this.ui.filters.container) {
+			//Get cached inputs
+			let groups = [
+				this.ui.content,
+				this.ui.orderby,
+				this.ui.order
+			];
+
+			groups.forEach(group => {
+				if(group) {
+					for (let input of group) {
+						let [filter, value] = [input.dataset.filter, input.value];
+						if (!Object.hasOwn(this.store.filters, filter)) break;
+						let doit = this.store.filters[filter] === value;
+						if (doit) {
+							input.checked = doit;
+							break;
+						}
+					}
+				}
+			});
+
+			if (Object.hasOwn(this.store.filters, 'taxonomy')) {
+				for (let [taxonomy, terms] of Object.entries(this.store.filters.taxonomy)) {
+					terms.forEach(termId => {
+						termId = parseInt(termId);
+						const term = this.selector.store.get(termId);
+						if (term) {
+							this.createTermElement(termId);
+						}
+					});
+				}
+			}
+		}
+	}
+
+	handlePopState(e) {
+		if (e.state?.filters) {
+			if (this.processURLFilters()) {
+				this.store.setFilters(this.filters);
+				this.a11y.announce('Feed filters updated from browser history');
+			}
+		}
+	}
+
+	handleClick(e) {
+		if (window.targetCheck(e, this.selectors.buttons.loadMore)) {
+			this.nextPage();
+		} else if (window.targetCheck(e, this.selectors.buttons.clearFilters)) {
+			this.clearFilters();
+		}
+		let remove = window.targetCheck(e, this.selectors.buttons.remove);
+		if (remove) {
+			this.removeSelectedTerm(remove);
+		}
+
+		let refresh = window.targetCheck(e, this.selectors.buttons.refresh);
+		if (refresh) {
+			this.store.clearCache();
+			this.store.fetch();
+		}
+
+		let orderbyButton = window.targetCheck(e, '[data-filter="orderby"]');
+		if (orderbyButton && orderbyButton.value === 'random' && orderbyButton.checked) {
+			// Already selected random, just re-render to trigger new shuffle
+			this.renderItems();
+		}
+	}
+
+	nextPage() {
+		const nextPage = (this.store.filters.page || 1) + 1;
+		const maxPage = this.store.lastResponse?.pages || nextPage;
+		this.store.setFilters({ page: Math.min(nextPage, maxPage) });
+	}
+
+	handleChange(e) {
+		const target = e.target;
+		if (Object.hasOwn(target.dataset, 'filter')) {
+			if (this.allowedFilters.includes(target.dataset.filter)) {
+				let filters = {};
+				filters[target.dataset.filter] = target.value;
+				this.resetFilters(filters);
+			}
+			switch (target.dataset.filter) {
+				case 'content':
+					this.updateContentFor(target.value);
+					break;
+				case 'orderby':
+					this.updateOrderOptions(target.value);
+					break;
+			}
+		}
+	}
+
+	clearFilters() {
+		this.taxFilters = {};
+		window.removeChildren(this.ui.selected);
+
+		this.taxonomies.forEach(tax => {
+			let fieldId = this.getFieldId(tax);
+			this.selector.selectedTerms.get(fieldId)?.clear();
+		});
+
+		this.store.setFilters({
+			...this.defaults,
+			taxonomy: null
+		});
+
+		this.updateURL();
+		this.saveToCacheFilters();
+	}
+
+	resetFilters(filters) {
+		filters = {
+			...this.store.filters,
+			page: 1,
+			... filters
+		}
+		this.store.setFilters(filters);
+
+		this.updateURL();
+		this.saveToCacheFilters();
+	}
+
+	getFieldId(taxonomy) {
+		return this.selector.getFieldId(this.ui.taxonomies.filter(tax => tax.dataset.taxonomy === taxonomy)[0]??null);
+	}
+	removeSelectedTerm(button) {
+		const termId = parseInt(button.dataset.id);
+		const taxonomy = button.dataset.taxonomy;
+
+		if (Object.hasOwn(this.taxFilters, taxonomy)){
+			this.taxFilters[taxonomy] = this.taxFilters[taxonomy]
+				.filter(id => id !== termId);
+			if (this.taxFilters[taxonomy].length === 0) {
+				delete this.taxFilters[taxonomy];
+			}
+		}
+		button.remove();
+
+		// Find the fieldId for this taxonomy
+		const field = this.getFieldId(taxonomy);
+		if (field) {
+			this.selector.activeField = field;
+			// Notify selector to remove from its selectedTerms
+			this.selector.removeSelected(termId, field);
+		}
+
+		this.resetFilters({
+			taxonomy: Object.keys(this.taxFilters).length > 0
+				? this.taxFilters
+				: null
+		});
+	}
+
+	updateContentFor(content) {
+		let checkIt = [
+			this.ui.taxonomies,
+			this.ui.orderby
+		];
+
+		this.ui.filters.showing.textContent = this.ui.content.filter(c => c.value === content)[0].dataset.label;
+		checkIt.forEach(check => {
+			if (!check) return;
+			check.forEach(button => {
+				const forTypes = button.dataset.for?.split(',')??[];
+				button.hidden = forTypes.length > 0 && !forTypes.includes(content);
+				if (button.hidden && button.checked) {
+					button.checked = false;
+				}
+			});
+		});
+	}
+	updateOrderOptions(order) {
+		if (this.ui.filters.orderWrap) {
+			let options = this.ui.filters.orderWrap.dataset.forOrder.split(',')??[];
+			this.ui.filters.orderWrap.hidden = !options.includes(order);
+		}
+	}
+
+	updateFilterControls() {
+		const keys = Object.keys(this.taxFilters);
+		if (this.ui.buttons.clearFilters) {
+			this.ui.buttons.clearFilters.hidden = keys.length === 0;
+		}
+		if (this.ui.filters.actions) {
+			this.ui.filters.actions.hidden = keys.length <= 1;
+		}
+	}
+
+	async initTaxonomies() {
+		this.taxFilters = {};
+		this.selector = window.jvbSelector;
+		// this.selector.scanExistingFields(this.ui.filters.container);
+		// this.taxonomies.map(tax => this.selector.batchFetch.add(tax));
+		// this.selector.batchFetchTaxonomies();
+		this.selector.subscribe((event, data) => {
+			switch (event) {
+				case 'selected-terms':
+
+					this.handleTaxonomyChange(data);
+					break;
+
+			}
+		});
+	}
+	handleTaxonomyChange(data) {
+		const {terms, taxonomy } = data;
+		if (terms.size === 0) return;
+		this.taxFilters[taxonomy] = Array.from(terms);
+		this.resetFilters({ taxonomy: this.taxFilters });
+
+		terms.forEach(t => {
+			this.createTermElement(t);
+		});
+		this.updateFilterControls();
+	}
+	getTaxonomyIcon(taxonomy) {
+		let iconButton = this.ui.taxonomies
+			.find(t => t.dataset.taxonomy === taxonomy);
+		return iconButton?.dataset.icon.trim() || 'tag';
+	}
+	createTermElement(termId) {
+		const term = this.selector.store.get(termId);
+		if (!term) return;
+		if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
+
+		term.icon = this.getTaxonomyIcon(term.taxonomy);
+		this.ui.selected.append(this.templates.create('feedTerm', term));
+	}
+
+	processCachedFilters() {
+		Object.keys(this.filters).forEach(filter => {
+			let cached = this.cache.get(`${this.config.contextId}_${this.config.context}_${filter}`);
+			if (cached && cached !== this.filters[filter]) {
+				this.filters[filter] = cached;
+			}
+		});
+	}
+
+	processURLFilters() {
+		if (!this.isFirstPage()) return false;
+		const params = new URLSearchParams(window.location.search);
+		if (!params.toString()) return false;
+		let shouldUpdate = false;
+		this.allowedFilters.forEach(filter => {
+			let value = params.get(`f_${filter}`);
+			if (value) {
+				shouldUpdate = true;
+				this.filters[filter] = value;
+			}
+		});
+
+		let hasTax = false;
+		params.forEach((value, key) => {
+			if (key.startsWith('f_tax_')) {
+				hasTax = true;
+				shouldUpdate = true;
+				const taxonomy = key.replace('f_tax_','');
+				this.taxFilters[taxonomy] = value.split(',').map(Number);
+			}
+		});
+		if (shouldUpdate) {
+			if (hasTax) {
+				this.filters.taxonomy = this.taxFilters;
+			}
+			this.resetFilters(this.filters);
+		}
+		return true;
+	}
+
+	updateURL() {
+		const params = new URLSearchParams();
+		this.allowedFilters.forEach(key => {
+			if (Object.hasOwn(this.store.filters, key) && this.store.filters[key] !== this.defaults[key]) {
+				params.set(`f_${key}`, this.store.filters[key]);
+			}
+		});
+
+		for (let [tax, terms] of Object.entries(this.taxFilters)) {
+			if (terms.length > 0) {
+				params.set(`f_tax_${tax}`, terms.join(','));
+			}
+		}
+
+		const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
+		const currentURL = window.location.pathname + window.location.search;  // Change this line
+
+		if (newURL !== currentURL) {
+			window.history.pushState({filters:this.store.filters}, '', newURL);
+		}
+	}
+
+	saveToCacheFilters() {
+		Object.keys(this.store.filters).forEach(filter => {
+			const cacheKey = `${this.config.contextId}_${this.config.context}_${filter}`;
+
+			if (this.store.filters[filter] !== this.defaults[filter]) {
+				this.cache.set(cacheKey, this.store.filters[filter]);
+			} else {
+				this.cache.remove(cacheKey);
+			}
+		});
+
+		const taxCacheKey = `${this.config.contextId}_${this.config.context}_taxonomy`;
+		if (Object.keys(this.taxFilters).length > 0) {
+			this.cache.set(taxCacheKey, this.taxFilters);
+		} else {
+			this.cache.remove(taxCacheKey);
+		}
+	}
+
+	initGallery() {
+		this.gallery = (this.config.gallery) ? window.jvbGallery : false;
+		if (this.gallery) {
+			this.gallery.subscribe((event, data) => {
+				if (event === 'load-more' && this.store.lastResponse?.has_more) {
+					this.nextPage();
 				}
 			});
 		}
-
-		// Update content-specific visibility
-		this.updateContentFor(this.filters.content);
 	}
-	nextPage() {
-		this.store.setFilter('page', this.store.filters.page++);
-	}
-
 	initStore() {
+		let extraOrderby = this.ui.orderby.filter(v => !['date','date_modified','title','random'].includes(v.value));
+
+		let extraIndexes = [];
+		extraOrderby.forEach(orderby =>{
+			extraIndexes.push({name:orderby.value, keyPath: orderby.value});
+		});
 		const store = window.jvbStore.register(
 			'feed',
 			{
@@ -208,256 +466,106 @@
 					{ name: 'content', keyPath: 'content'},
 					{ name: 'taxonomy', keyPath: 'taxonomy'},
 					{ name: 'user', keyPath: 'user'},
-					{ name: 'date', keyPath: 'modified'},
-					{ name: 'title', keyPath: 'title'}
+					{ name: 'date', keyPath: 'date'},
+					{ name: 'modified', keyPath: 'modified'},
+					{ name: 'title', keyPath: 'title'},
+					... extraIndexes
 				],
 				filters: this.filters,
-				TTL: 6 * 60 * 60 * 1000,
+				TTL: 6 * 60 * 60 * 1000, //6 hours
 				showLoading: true,
 				required: 'content',
-				delayFetch: true
-			}
+			},
+			2
 		);
+
 		this.store = store.feed;
 
 		this.store.subscribe((event, data) => {
 			switch (event) {
 				case 'data-loaded':
-					this.renderItems();
-					this.ui.loadMore.hidden = true;
-					if (this.store.lastResponse && this.store.lastResponse['has_more']) {
-						this.ui.loadMore.hidden = !this.store.lastResponse['has_more'];
+					if (this.isFirstLoad) {
+						//We rendered the first page in php already
+						this.isFirstLoad = false;
+						return;
+					}
+					// if (this.store.filters.page === 1) {
+					// 	return;
+					// }
+					this.renderItems(data.items);
+					this.ui.buttons.loadMore.hidden = true;
+					if (this.store.lastResponse && this.store.lastResponse?.has_more) {
+						this.ui.buttons.loadMore.hidden = !this.store.lastResponse?.has_more??true;
 					}
 					break;
 			}
 		});
 	}
 
-	initGallery() {
-		this.gallery = (this.config.gallery) ? window.jvbGallery : false;
-		if (this.gallery) {
-			this.gallery.subscribe((event, data) => {
-				if (event === 'load-more' && this.store.lastResponse) {
-					if (this.store.lastResponse['has_more']) {
-						this.nextPage();
-					}
-				}
-			});
-		}
+	isFirstPage() {
+		return this.store.filters.page === 1;
 	}
 
-	processCachedFilters() {
-		Object.keys(this.filters).forEach(filter => {
-			let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`);
-			if (cached && cached !== this.filters[filter]){
-				this.filters[filter] = cached;
-			}
-		});
-	}
-
-	processURLFilters() {
-		if (this.filters.page > 1) {
-			return false;
-		}
-		const params = new URLSearchParams(window.location.search);
-
-		if (!params.toString()) {
-			return false;
-		}
-		let filters = ['content', 'order', 'orderby', 'favourites', 'match'];
-		filters.forEach(filter => {
-			let value = params.get(`f_${filter}`);
-			if (value) {
-				this.filters[filter] = value;
-				let input = this.ui.filters[filter];
-				if (input) {
-					input.checked = true;
-				}
-			}
-		});
-
-		let hasTaxonomy = false;
-		// Load taxonomy filters from URL
-		params.forEach((value, key) => {
-			if (key.startsWith('f_tax_')) {
-				hasTaxonomy = true;
-				const taxonomy = key.replace('f_tax_', '');
-				if (!this.taxonomyFilters[taxonomy]) {
-					this.taxonomyFilters[taxonomy] = [];
-				}
-				this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
-			}
-		});
-		if (this.ui.filterContainer && hasTaxonomy) {
-			for (let [tax, ids] in Object.entries(this.taxonomyFilters)) {
-				let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`);
-				if (button) {
-					if (button.dataset.fieldId) {
-						let field = this.selector.get(button.dataset.fieldId);
-						field.selectedTerms = new Set(ids);
-						this.selector.initFieldDisplay(button.dataset.fieldId);
-					} else {
-						this.selector.registerField(button, {
-							button: button,
-							buttonSelector: '[data-filter="taxonomy"]',
-							selected: this.ui.selectedTax,
-							selectedItems: ids
-						});
-					}
-				}
-			}
-		}
-		return true;
-	}
-
-	/**
-	 * Update URL with current filters (for sharing/bookmarking)
-	 */
-	updateURL() {
-		const params = new URLSearchParams();
-
-		// Add simple filters
-		['content', 'order', 'orderby', 'match'].forEach(key => {
-			if (this.filters[key]) {
-				params.set(`f_${key}`, this.filters[key]);
-			}
-		});
-
-		// Add taxonomy filters
-		Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => {
-			if (terms.length > 0) {
-				params.set(`f_tax_${taxonomy}`, terms.join(','));
-			}
-		});
-
-		// Update URL without reload
-		const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
-		window.history.pushState({ filters: this.filters }, '', newURL);
-	}
-
-	renderItems() {
-		let items = this.store.getFiltered();
-		if (this.store.filters['page'] === 1) {
+	renderItems(items = null) {
+		items = items??this.store.getFiltered();
+		if (this.isFirstPage()) {
 			window.removeChildren(this.ui.grid);
 		}
-
 		if (items.length === 0) {
-			this.a11y.announceItems(0, this.store.filters['page'] > 0);
-			return;
-		}
-
-		const fragment = document.createDocumentFragment();
-		const batchSize = 10;
-
-		const processBatch = (startIndex) => {
-			const endIndex = Math.min(startIndex + batchSize, items.length);
-
-			for (let i = startIndex; i < endIndex; i++) {
-				const item = items[i];
-				const element = this.createItemElement(item);
-
-				fragment.appendChild(element);
-			}
-
-			if (endIndex < items.length) {
-				requestAnimationFrame(() => processBatch(endIndex));
-			} else {
-				this.removePlaceholders();
-				this.ui.grid.append(fragment);
-
-				if (this.config.gallery) {
-					this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
-				}
-
-				this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
-				this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore);
-			}
-		};
-
-		if (items.length > 0) {
-			processBatch(0);
+			this.showEmptyState();
+			this.a11y.announceItems(0, this.isFirstPage());
 		} else {
-			this.a11y.announceItems(0, this.store.filters['page'] >1, false);
+			window.chunkIt(
+				items,
+				(item) => this.createItemElement(item),
+				(fragment) => {
+					this.removePlaceholders();
+					this.ui.grid.append(fragment);
+					if (this.config.gallery) this.gallery.buildGalleryItems('.item img');
+					this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+					this.a11y.announceItems(items.length, !this.isFirstPage(), this.store.lastResponse?.has_more??false);
+				},
+				5
+			).then(()=>{});
 		}
 
-		if (this.ui.filters.match) {
-			this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
-		}
-		if (this.ui.clearFilter) {
-			this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
-		}
+		this.updateFilterControls();
 	}
 
-	/**
-	 *
-	 * @param {object} item
-	 */
+	showEmptyState() {
+		window.removeChildren(this.ui.grid);
+		this.ui.grid.append(this.templates.create('emptyState'));
+	}
+
 	createItemElement(item) {
-		let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
-		if (Object.hasOwn(template.dataset, 'timeline')) {
-			return this.createTimelineElement(item, template);
+		if (typeof item !== 'object') {
+			item = this.store.get(item);
+			if (!item) return;
 		}
-		//fields
-		for (let [fieldName, value] of Object.entries(item.fields)) {
-			let el = template.querySelector(`[data-field="${fieldName}"]`);
-			if (!el) {
-				continue;
-			}
-			if (Object.keys(item.images).map((img)=> parseInt(img)).includes(value)) {
-				[
-					el.src,
-					el.srcset,
-					el.alt
-				] = [
-					item.images[value].tiny,
-					`${item.images[value].tiny} 50w, ${item.images[value].small} 300w, ${item.images[value].medium} 1024w`,
-					item.images[value]['image-alt-text']
-				];
-			} else if (el.tagName === 'TIME') {
-				el.setAttribute('datetime', value);
-				el.textContent = window.formatTimeAgo(value);
-			} else {
-				el.textContent = value;
-			}
-			if (value === '') {
-				el.remove();
-			}
-		}
-		let link = template.querySelector('a');
-		if (link && item.url !== '') {
-			[
-				link.href,
-				link.title
-			] = [
-				item.url,
-				`View ${item.fields['post_title']??'Item'}`
-			];
-		}
-		return template;
+		return this.templates.create(`feedItem${window.uppercaseFirst(item.content)}`, item);
 	}
 	splitIDs(value) {
-		console.log(value);
-		return value.split(',').map((value) => parseInt(value.trim())).filter(value=>value);
+		return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
 	}
+
 	isImageField(item, value) {
 		if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) {
 			return false;
 		}
 		let values = this.splitIDs(value);
-		values.forEach(v => {
-			if (Object.keys(item.images).includes(v)) {
-				return true;
-			}
-		});
-		return false;
+
+		return values.some(v =>
+			Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v))
+		);
 	}
 	formatImageFields(element, value, item) {
-		if (value.length === 0) return;
-		//If it's a gallery, we're cloning the original image, then removing it
-		if (value.length > 1) {
+		let values = this.splitIDs(value); // Convert string to array first
+		if (values.length === 0) return;
+
+		if (values.length > 1) {
 			let image = element.querySelector('img');
 			if (!image) return;
-			value.forEach(imgID => {
+			values.forEach(imgID => {
 				let img = image.cloneNode(true);
 				this.formatImageField(img, imgID, item);
 				element.append(img);
@@ -468,35 +576,35 @@
 				element = element.querySelector('img');
 				if (!element) return;
 			}
-			this.formatImageField(element, value[0], item);
+			this.formatImageField(element, values[0], item);
 		}
 	}
-		formatImageField(element, value, item) {
-			[
-				element.src,
-				element.srset,
-				element.alt
-			] = [
-				item.images[value].tiny,
-				`${item.images[value].tiny} 50w, ${item.images[value].small} 300w, ${item.images[value].medium} 1024w`,
-				item.images[value]['image-alt-text']
-			]
-		}
+	formatImageField(element, value, item) {
+		let imgData = item.images[value]??false;
+		if (!imgData) return;
+		[
+			element.src,
+			element.srcset,
+			element.alt
+		] = [
+			imgData.tiny,
+			`${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`,
+			imgData['image-alt-text']
+		]
+	}
 	isTaxonomyField(item, field) {
 		if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) {
 			return false;
 		}
-		Object.keys(item.taxonomies).forEach(taxonomy => {
-			if (taxonomy === field) {
-				return true;
-			}
-		});
-		return false;
+
+		return Object.keys(item.taxonomies).includes(field);
 	}
 	formatTaxonomyField(element, item, field, value) {
 		if (element.tagName !== 'UL' || !element.querySelector('li')) return;
-
 		let values = this.splitIDs(value);
+		if (values.length === 0) {
+			element.remove();
+		}
 		let listItem = element.querySelector('li');
 		for (let termID of values) {
 			let term = item.taxonomies[field][termID]??false;
@@ -505,14 +613,16 @@
 			let link = termItem.querySelector('a');
 			if (!link) continue;
 
+			let title = window.decodeHTMLEntities(term.title);
+
 			[
 				link.href,
 				link.title,
 				link.textContent
 			] = [
 				term.url,
-				`See more ${term.title}`,
-				term.title
+				`See more ${title}`,
+				title
 			];
 			element.append(termItem);
 		}
@@ -527,33 +637,13 @@
 			if (!element) return;
 		}
 		element.setAttribute('datetime', value);
-		element.textContent = window.formatTimeAgo(value);
+		element.textContent = window.formatTimeAgo(value, 'F Y');
 	}
 	formatField(element, value) {
-		element.textContent = value;
+		element.textContent = window.decodeHTMLEntities(value);
 	}
 
-	createTimelineElement(item, template) {
-		console.log(item);
-		console.log(template);
-		for (let [field, value] of Object.entries(item.fields)) {
-			if (!['timeline', 'number'].includes(field)) {
-				let el = template.querySelector(`[data-field="${field}"]`);
-				if (!el) {
-					console.log(`Element Not found for ${field}`);
-				}
-				if (!el || value === '') continue;
-				if (this.isImageField(item, value)) {
-					this.formatImageFields(el, value, item);
-				} else if (this.isTaxonomyField(item, field)) {
-					this.formatTaxonomyField(el, item, field, value);
-				} else if (this.isTimeField(el)) {
-					this.formatTimeField(el, value);
-				} else {
-					this.formatField(el, value);
-				}
-			}
-		}
+	addTimelineElements(item, template) {
 		let [
 			afterEl,
 			number,
@@ -565,11 +655,12 @@
 			template.querySelector('[data-field="started"] time'),
 			template.querySelector('[data-field="updated"] time')
 		];
+
 		if (afterEl) {
-			afterEl.textContent = `After ${item.fields.number} Tx`;
+			afterEl.textContent = `After ${item.number} Tx`;
 		}
 		if (number) {
-			number.textContent = item.fields.number;
+			number.textContent = item.number;
 		}
 		if (started) {
 			this.formatTimeField(started, item.fields.timeline[0]['post_date']);
@@ -577,8 +668,6 @@
 		if (last) {
 			this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']);
 		}
-
-		return template;
 	}
 
 	removePlaceholders() {
@@ -588,198 +677,81 @@
 		}
 	}
 
+	defineTemplates() {
+		const T = this.templates;
+		const f = this;
 
-	addPlaceholders() {
-		let total = this.contentTypes.length;
-		const fragment = document.createDocumentFragment();
-		for (let i = 0; i < 12; i++) {
-			let template = window.getTemplate('placeholderTemplate');
-
-			let rand = Math.floor(Math.random() * total);
-			let icon;
-			if (this.ui.content && this.ui.content.length > 0) {
-				icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true);
-			} else {
-				icon = window.getIcon(this.container.dataset.icon);
+		T.define('feedTerm', {
+			refs: {
+				icon: '.icon',
+				span: 'span'
+			},
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+				el.dataset.taxonomy = data.taxonomy;
+				if (refs.icon) refs.icon.className = `icon icon-${data.icon}`;
+				if (refs.span) refs.span.textContent = window.decodeHTMLEntities(data.name);
 			}
-			template.append(icon);
-			fragment.append(template);
-		}
-		this.ui.grid.append(fragment);
-	}
-
-
-
-	/**
-	 *
-	 * @param {object} filters {name: value}
-	 */
-	updateFilter(filters) {
-		//double check filters are what we're expecting
-		let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)];
-
-		filters = Object.keys(filters)
-			.filter(key => allowed.includes(key))
-			.reduce((obj, key) => {
-				obj[key] = filters[key];
-				return obj;
-			}, {});
-
-		if (window.getDifferences.map(this.filters, filters)) {
-			this.filters = { ...this.filters, ...filters };  // Merge instead of replace
-			this.updateURL();
-			this.store.setFilters(filters);
-		}
-	}
-	/**
-	 * Update visible filters based on selected content type
-	 */
-	updateContentFor(contentType) {
-		// Update taxonomy filter visibility
-		const taxonomyButtons = this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]');
-		taxonomyButtons.forEach(button => {
-			const forTypes = button.dataset.for?.split(',') || [];
-			button.hidden = forTypes.length > 0 && !forTypes.includes(contentType);
 		});
+		T.define('emptyState');
 
-		// Update ordering options
-		const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]');
-		orderButtons.forEach(button => {
-			const forTypes = button.dataset.for?.split(',') || [];
-			if (forTypes.length > 0) {
-				button.hidden = !forTypes.includes(contentType);
-				// Uncheck if hiding
-				if (button.hidden && button.checked) {
-					button.checked = false;
+		this.contentTypes.forEach(content => {
+			T.define(`feedItem${window.uppercaseFirst(content)}`, {
+				refs: {
+					link: 'a',
+				},
+				manyRefs: {
+					fields: '[data-field]',
+				},
+				setup({el, refs, manyRefs, data}) {
+					const isTimeline = Object.hasOwn(el.dataset, 'timeline');
+					if (manyRefs.fields) {
+						for (let field of manyRefs.fields) {
+							if (isTimeline && ['timeline','number'].includes(field.dataset.field)) continue;
+							const value = Object.hasOwn(data.fields, field.dataset.field)? data.fields[field.dataset.field] : false;
+							if (!value) {
+								field.remove();
+								continue;
+							}
+							if (f.isImageField(data, value)) {
+								f.formatImageField(field, value, data);
+							} else if (f.isTaxonomyField(data, field.dataset.field)) {
+								f.formatTaxonomyField(field, data, field.dataset.field, value);
+							} else if (f.isTimeField(field)) {
+								f.formatTimeField(field, value);
+							} else {
+								f.formatField(field, value);
+							}
+						}
+						if (refs.link && data.url !== '') {
+							refs.link.href = data.url;
+							refs.link.title = `View ${data.fields['post_title']??'Item'}`;
+						}
+						if (isTimeline ) f.addTimelineElements(data, el);
+					}
 				}
-			}
-		});
-
-		// Update order direction visibility based on selected orderby
-		const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked');
-		this.updateOrderDirectionVisibility(orderBy?.value);
-	}
-
-	/**
-	 * Show/hide order direction based on orderby selection
-	 */
-	updateOrderDirectionVisibility(orderBy) {
-		const orderDirection = this.ui.filterContainer.querySelector('.order-direction');
-		if (orderDirection) {
-			const forOrders = orderDirection.dataset.forOrder?.split(',') || [];
-			orderDirection.hidden = forOrders.length > 0 && !forOrders.includes(orderBy);
-		}
-	}
-	/*********************************************************************
-	LISTENERS
-	 *********************************************************************/
-	initListeners() {
-		this.popStateHandler 	= this.handlePopState.bind(this);
-		this.clickHandler		= this.handleClick.bind(this);
-		this.changeHandler		= this.handleChange.bind(this);
-		this.imageObserver = null;
-		this.resizeObserver = null;
-		if ('IntersectionObserver' in window) {
-			this.imageObserver = new IntersectionObserver(entries => {
-				entries.forEach(entry => {
-					this.loadImage(entry.target);
-					this.imageObserver.unobserve(entry.target);
-				});
-			}, {
-				rootMargin: '100px',
-				threshold: .1
-			});
-		}
-
-		if ('ResizeObserver' in window) {
-			this.resizeObserver = new ResizeObserver(() => {
-				window.debouncer.schedule(
-					'feed-update-images',
-					() => this.updateImageSizes(),
-					250
-				);
-			});
-		} else {
-			window.addEventListener('resize', () => {
-				window.debouncer.schedule(
-					'feed-update-images',
-					() => this.updateImageSizes(),
-					250
-				);
-			});
-		}
-
-		window.addEventListener('popstate', this.popStateHandler);
-		document.addEventListener('click', this.clickHandler);
-		document.addEventListener('change', this.changeHandler);
-	}
-
-	handlePopState(e) {
-		if (e.state?.filters) {
-			if (this.processURLFilters()) {
-				this.store.setFilters(this.filters);
-				this.a11y.announce('Feed filters updated from browser history');
-			}
-		}
-	}
-
-	handleClick(e) {
-		if (window.targetCheck(e, this.elements.loadMore)) {
-			this.nextPage();
-		} else if (window.targetCheck(e, this.elements.clearFilter)) {
-			this.clearAllTaxonomies();
-		} else if (window.targetCheck(e, '.remove-item')) {
-			this.handleRemoveSelectedTerm(e);
-		}
-	}
-
-	handleRemoveSelectedTerm(e) {
-		const selectedItem = e.target.closest('.selected-item');
-		if (!selectedItem) return;
-
-		const termId = parseInt(selectedItem.dataset.id);
-		const taxonomy = selectedItem.dataset.taxonomy;
-
-		// Remove from filters
-		if (this.taxonomyFilters[taxonomy]) {
-			this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy]
-				.filter(id => id !== termId);
-
-			if (this.taxonomyFilters[taxonomy].length === 0) {
-				delete this.taxonomyFilters[taxonomy];
-			}
-		}
-
-		// Remove from UI
-		selectedItem.remove();
-
-		// Update filters
-		this.updateFilter({
-			taxonomy: Object.keys(this.taxonomyFilters).length > 0
-				? this.taxonomyFilters
-				: null,
-			page: 1
+			})
 		});
 	}
 
-	handleChange(e) {
-		let target = e.target;
-		if (Object.hasOwn(target.dataset, 'filter')) {
-			if (target.dataset.filter === 'content') {
-				this.updateContentFor(target.value);
-				this.updateFilter({ content: target.value, page: 1 });
-			} else if (target.dataset.filter === 'orderby') {
-				this.updateOrderDirectionVisibility(target.value);
-				this.updateFilter({ orderby: target.value, page: 1 });
-			} else if (target.dataset.filter === 'order') {
-				this.updateFilter({ order: target.value, page: 1 });
-			} else if (target.dataset.filter === 'match') {
-				this.updateFilter({ match: target.checked ? 'all' : 'any', page: 1 });
-			} else if (target.dataset.filter === 'favourites') {
-				this.updateFilter({ favourites: target.checked, page: 1 });
-			}
-		}
-	}
+	// addPlaceholders() {
+	// 	let total = this.contentTypes.length;
+	// 	const fragment = document.createDocumentFragment();
+	// 	for (let i = 0; i < 12; i++) {
+	// 		let template = window.getTemplate('placeholderTemplate');
+	//
+	// 		let rand = Math.floor(Math.random() * total);
+	// 		let icon;
+	// 		if (this.ui.content && this.ui.content.length > 0) {
+	// 			icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true);
+	// 		} else {
+	// 			icon = window.getIcon(this.container.dataset.icon);
+	// 		}
+	// 		template.append(icon);
+	// 		fragment.append(template);
+	// 	}
+	// 	this.ui.grid.append(fragment);
+	// }
 }
 
 document.addEventListener('DOMContentLoaded', async function() {
@@ -788,33 +760,4 @@
 			window.feedBlock = new FeedBlock();
 		}
 	});
-
-	let item = {
-		content: "art",
-		date: "2025-12-24 03:37:26",
-		fields: {
-			gallery: "",
-			post_content: "",
-			post_thumbnail: 200,
-			post_title: "Great Gray Owl",
-			price: "",
-		},
-		icon: "arrows-clockwise",
-		id: 195,
-		images: {
-			200: {
-				'image-alt-text': "",
-				'image-caption': "",
-				'image-title': "Great Gray Owl",
-				large: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl.jpg",
-				medium: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-1024x1024.jpg",
-				small: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-300x300.jpg",
-				tiny: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-50x50.jpg"
-			}
-		},
-		url: "http://jakevan.test/art/great-gray-owl/",
-		user_id: 3
-	};
 });
-
-

--
Gitblit v1.10.0