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

---
 src/feed/view.js |  364 +++++++++++++++++++++++++++++++--------------------
 1 files changed, 219 insertions(+), 145 deletions(-)

diff --git a/src/feed/view.js b/src/feed/view.js
index 4e118c4..a8c9946 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -6,9 +6,11 @@
 		this.a11y = window.jvbA11y;
 		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,
@@ -16,26 +18,23 @@
 			... this.container.dataset
 		};
 
+
 		this.init();
 	}
 	init() {
 		this.initElements();
+		this.defineTemplates();
+
 		this.initListeners();
 		this.initFilters();
 
-		if ('requestIdleCallback' in window) {
-			requestIdleCallback(() => {
-				this.initStore();
-				this.initTaxonomies();
-				this.initGallery();
-			}, { timeout: 2000 });
-		} else {
-			setTimeout(() => {
-				this.initStore();
-				this.initTaxonomies();
-				this.initGallery();
-			}, 100);
-		}
+		this.initStore();
+		this.initTaxonomies().then(r => {});
+
+		this.processCachedFilters();
+		this.processURLFilters();
+		this.updateFilterUI();
+		this.initGallery();
 	}
 
 	initElements() {
@@ -43,10 +42,13 @@
 			filterTrigger: '[data-filter]',
 			filters: {
 				actions: 	'.filter-actions .toggle-text',
-				container:	'.filters',
+				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"]',
@@ -56,31 +58,49 @@
 			buttons: {
 				loadMore: 		'button.load-more',
 				remove:			'.remove-term',
-				clearFilters: 	'button.clear-filters'
+				clearFilters: 	'button.clear-filters',
+				refresh:		'button[data-action="refresh"]'
 			}
 		};
 		this.ui = window.uiFromSelectors(this.selectors, this.container);
+		this.ui.buttons.refresh = document.querySelector(this.selectors.buttons.refresh);
 
 		//Add content and taxonomies
-		this.ui.content = this.ui.filters.container.querySelectorAll('[name="content"]');
-		if (this.ui.content.length === 0) this.ui.content = false;
-		this.ui.taxonomies = this.ui.filters.container.querySelectorAll('[data-taxonomy]');
-		if (this.ui.taxonomies.length === 0) this.ui.content = false;
-		this.ui.orderbyWrap = this.ui.filters.container.querySelector('[data-for-order]');
-		if (this.ui.orderbyWrap.length === 0) this.ui.content = false;
-		this.ui.order = this.ui.filters.container.querySelectorAll('[data-filter="order"]');
-		if (this.ui.order.length === 0) this.ui.content = false;
-		this.ui.orderby = this.ui.filters.container.querySelectorAll('[data-filter="orderby"]');
-		if (this.ui.orderby.length === 0) this.ui.content = false;
+		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.contentTypes = (this.ui.content)
-			? Array.from(this.ui.content).map(c => c.value)
+		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)
 			: [];
 	}
 
+	/**
+	 *
+	 * @param {string} item
+	 */
+	getChecked(item) {
+		if (!['content', 'orderby','order'].includes(item)) {
+			console.log('Invalid item to check: ', item);
+		}
+
+		let items = this.ui[item];
+		if (!items) {
+			return;
+		}
+
+		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;
+	}
+
 	initListeners() {
 		this.popStateHandler	= this.handlePopState.bind(this);
 		this.clickHandler		= this.handleClick.bind(this);
@@ -94,18 +114,16 @@
 	initFilters() {
 		this.allowedFilters = ['content', 'order', 'orderby', 'favourites', 'match'];
 		let defaults = {
-			content:	this.contentTypes[0],
-			orderby:	'date',
-			order:		'desc',
+			content:	this.getChecked('content'),
+			orderby:	this.getChecked('orderby'),
+			order:		this.getChecked('order'),
 			page:		1,
 		};
 		if (this.config.context) defaults.context = this.config.context;
-		if (this.config.source) defaults.source = this.config.source;
+		if (this.config.contextId) defaults.contextId = this.config.contextId;
 
 		this.filters = defaults;
-		this.processCachedFilters();
-		this.processURLFilters();
-		this.updateFilterUI();
+
 		this.defaults = {...defaults};
 	}
 	updateFilterUI() {
@@ -121,8 +139,8 @@
 				if(group) {
 					for (let input of group) {
 						let [filter, value] = [input.dataset.filter, input.value];
-						if (!Object.hasOwn(this.filters, filter)) break;
-						let doit = this.filters[filter] === value;
+						if (!Object.hasOwn(this.store.filters, filter)) break;
+						let doit = this.store.filters[filter] === value;
 						if (doit) {
 							input.checked = doit;
 							break;
@@ -131,13 +149,13 @@
 				}
 			});
 
-			if (Object.hasOwn(this.filters, 'taxonomy')) {
-				for (let [taxonomy, terms] of Object.entries(this.filters.taxonomy)) {
+			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(term);
+							this.createTermElement(termId);
 						}
 					});
 				}
@@ -164,6 +182,18 @@
 		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() {
@@ -204,6 +234,9 @@
 			...this.defaults,
 			taxonomy: null
 		});
+
+		this.updateURL();
+		this.saveToCacheFilters();
 	}
 
 	resetFilters(filters) {
@@ -213,10 +246,13 @@
 			... filters
 		}
 		this.store.setFilters(filters);
+
+		this.updateURL();
+		this.saveToCacheFilters();
 	}
 
 	getFieldId(taxonomy) {
-		return this.selector.getFieldId(Array.from(this.ui.taxonomies).filter(tax => tax.dataset.taxonomy === taxonomy)[0]??null);
+		return this.selector.getFieldId(this.ui.taxonomies.filter(tax => tax.dataset.taxonomy === taxonomy)[0]??null);
 	}
 	removeSelectedTerm(button) {
 		const termId = parseInt(button.dataset.id);
@@ -251,6 +287,8 @@
 			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 => {
@@ -263,19 +301,19 @@
 		});
 	}
 	updateOrderOptions(order) {
-		if (this.ui.orderbyWrap) {
-			let options = this.ui.orderbyWrap.dataset.forOrder.split(',')??[];
-			this.ui.orderbyWrap.hidden = !options.includes(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 isHidden = Object.keys(this.taxFilters).length === 0;
+		const keys = Object.keys(this.taxFilters);
 		if (this.ui.buttons.clearFilters) {
-			this.ui.buttons.clearFilters.hidden = isHidden;
+			this.ui.buttons.clearFilters.hidden = keys.length === 0;
 		}
 		if (this.ui.filters.actions) {
-			this.ui.filters.actions.hidden = isHidden;
+			this.ui.filters.actions.hidden = keys.length <= 1;
 		}
 	}
 
@@ -307,7 +345,7 @@
 		this.updateFilterControls();
 	}
 	getTaxonomyIcon(taxonomy) {
-		let iconButton = Array.from(this.ui.taxonomies)
+		let iconButton = this.ui.taxonomies
 			.find(t => t.dataset.taxonomy === taxonomy);
 		return iconButton?.dataset.icon.trim() || 'tag';
 	}
@@ -315,22 +353,14 @@
 		const term = this.selector.store.get(termId);
 		if (!term) return;
 		if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
-		let icon = this.getTaxonomyIcon(term.taxonomy);
-		let template = window.getTemplate('feedTerm');
-		if (!template) return;
-		let [iconEl,span] = [template.querySelector('.icon'), template.querySelector('span')];
-		if (!iconEl || !span) return;
-		template.dataset.id = term.id;
-		template.dataset.taxonomy = term.taxonomy;
-		iconEl.className = `icon icon-${icon}`;
-		span.textContent = term.name;
 
-		this.ui.selected.append(template);
+		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.source}_${this.config.context}_${filter}`);
+			let cached = this.cache.get(`${this.config.contextId}_${this.config.context}_${filter}`);
 			if (cached && cached !== this.filters[filter]) {
 				this.filters[filter] = cached;
 			}
@@ -338,31 +368,41 @@
 	}
 
 	processURLFilters() {
-		if (this.filters.page > 1) return false;
+		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.filters, key) && this.filters[key] !== this.defaults[key]) {
-				params.set(`f_${key}`, this.filters[key]);
+			if (Object.hasOwn(this.store.filters, key) && this.store.filters[key] !== this.defaults[key]) {
+				params.set(`f_${key}`, this.store.filters[key]);
 			}
 		});
 
@@ -371,29 +411,31 @@
 				params.set(`f_tax_${tax}`, terms.join(','));
 			}
 		}
+
 		const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
-		if (newURL !== window.location.pathname) {
-			window.history.pushState({filters:this.filters}, '', newURL)
+		const currentURL = window.location.pathname + window.location.search;  // Change this line
+
+		if (newURL !== currentURL) {
+			window.history.pushState({filters:this.store.filters}, '', newURL);
 		}
 	}
 
 	saveToCacheFilters() {
-		// Save each filter that's changed from defaults
-		Object.keys(this.filters).forEach(filter => {
-			if (this.filters[filter] !== this.defaults[filter]) {
-				this.cache.set(
-					`${this.config.source}_${this.config.context}_${filter}`,
-					this.filters[filter]
-				);
+		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);
 			}
 		});
 
-		// Also save taxonomy filters
+		const taxCacheKey = `${this.config.contextId}_${this.config.context}_taxonomy`;
 		if (Object.keys(this.taxFilters).length > 0) {
-			this.cache.set(
-				`${this.config.source}_${this.config.context}_taxonomy`,
-				this.taxFilters
-			);
+			this.cache.set(taxCacheKey, this.taxFilters);
+		} else {
+			this.cache.remove(taxCacheKey);
 		}
 	}
 
@@ -408,6 +450,12 @@
 		}
 	}
 	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',
 			{
@@ -418,30 +466,38 @@
 					{ 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, //6 hours
 				showLoading: true,
 				required: 'content',
-			}
+			},
+			2
 		);
+
 		this.store = store.feed;
 
 		this.store.subscribe((event, data) => {
 			switch (event) {
 				case 'data-loaded':
-					this.renderItems();
+					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;
-				case 'filters-changed':
-					this.updateURL();
-					this.saveToCacheFilters();
-					break;
 			}
 		});
 	}
@@ -450,8 +506,8 @@
 		return this.store.filters.page === 1;
 	}
 
-	renderItems() {
-		let items = this.store.getFiltered();
+	renderItems(items = null) {
+		items = items??this.store.getFiltered();
 		if (this.isFirstPage()) {
 			window.removeChildren(this.ui.grid);
 		}
@@ -459,26 +515,18 @@
 			this.showEmptyState();
 			this.a11y.announceItems(0, this.isFirstPage());
 		} else {
-			const fragment = document.createDocumentFragment();
-			const processBatch = (startIndex) => {
-				const endIndex = Math.min(startIndex + 10, items.length);
-				for (let i = startIndex; i < endIndex; i++) {
-					const item = items[i];
-					const element = this.createItemElement(item);
-					fragment.append(element);
-				}
-				if (endIndex < items.length) {
-					requestAnimationFrame(() => processBatch(endIndex));
-				} else {
+			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);
-				}
-			};
-			processBatch(0);
+				},
+				5
+			).then(()=>{});
 		}
 
 		this.updateFilterControls();
@@ -486,48 +534,15 @@
 
 	showEmptyState() {
 		window.removeChildren(this.ui.grid);
-		let template = window.getTemplate('emptyState');
-		if (!template) return;
-		this.ui.grid.append(template);
-
+		this.ui.grid.append(this.templates.create('emptyState'));
 	}
 
 	createItemElement(item) {
-		let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
-		const isTimeline = Object.hasOwn(template.dataset, 'timeline');
-
-		for (let [fieldName, value] of Object.entries(item.fields)) {
-			if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue;
-
-			let el = template.querySelector(`[data-field="${fieldName}"]`);
-			if (!el) continue;
-			if (value === '') {
-				el.remove();
-				continue;
-			}
-
-			if (this.isImageField(item, value)) {
-				this.formatImageField(el, value, item);
-			} else if (this.isTaxonomyField(item, fieldName)) {
-				this.formatTaxonomyField(el, item, fieldName, value);
-			} else if (this.isTimeField(el)) {
-				this.formatTimeField(el, value);
-			} else {
-				this.formatField(el, value);
-			}
+		if (typeof item !== 'object') {
+			item = this.store.get(item);
+			if (!item) return;
 		}
-
-		let link = template.querySelector('a');
-		if (link && item.url !== '') {
-			[link.href, link.title] =
-				[item.url, `View ${item.fields['post_title']??'Item'}`];
-		}
-
-		if (isTimeline) {
-			this.addTimelineElements(item, template);
-		}
-
-		return template;
+		return this.templates.create(`feedItem${window.uppercaseFirst(item.content)}`, item);
 	}
 	splitIDs(value) {
 		return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
@@ -598,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);
 		}
@@ -623,7 +640,7 @@
 		element.textContent = window.formatTimeAgo(value, 'F Y');
 	}
 	formatField(element, value) {
-		element.textContent = value;
+		element.textContent = window.decodeHTMLEntities(value);
 	}
 
 	addTimelineElements(item, template) {
@@ -640,10 +657,10 @@
 		];
 
 		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']);
@@ -660,6 +677,63 @@
 		}
 	}
 
+	defineTemplates() {
+		const T = this.templates;
+		const f = this;
+
+		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);
+			}
+		});
+		T.define('emptyState');
+
+		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);
+					}
+				}
+			})
+		});
+	}
+
 	// addPlaceholders() {
 	// 	let total = this.contentTypes.length;
 	// 	const fragment = document.createDocumentFragment();

--
Gitblit v1.10.0