From 0afb2c0046b55c123eafb4ab9ee77efa68d12463 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 06 Jun 2026 17:15:31 +0000
Subject: [PATCH] =Starting the Favourites.js setup, converting previous Northeh stuff to new Registrar, fixing up Square.php integration to match
---
src/feed/view.js | 1184 ++++++++++++++++++++++++++++-------------------------------
1 files changed, 564 insertions(+), 620 deletions(-)
diff --git a/src/feed/view.js b/src/feed/view.js
index fc6edca..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,255 +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) {
- 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);
@@ -467,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;
@@ -504,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);
}
@@ -526,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,
@@ -564,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']);
@@ -576,8 +668,6 @@
if (last) {
this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']);
}
-
- return template;
}
removePlaceholders() {
@@ -587,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() {
@@ -787,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