From 3acb42faee66868a76e653a34ef35de13ddf734f Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 01 Jan 2026 23:00:11 +0000
Subject: [PATCH] Merge branch 'main' of https://github.com/jakevdwerf/jvb
---
src/feed/view.js | 407 +++++++++++++++++++++++++++++++++++++++++-----------------
1 files changed, 287 insertions(+), 120 deletions(-)
diff --git a/src/feed/view.js b/src/feed/view.js
index abb91e5..52584ce 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -55,7 +55,7 @@
favourites: '[data-filter="favourites"]',
taxonomy: '[data-filter^="taxonomy"]'
},
- selectedTax: '.selected-terms',
+ selectedTax: '.selected-items',
clearFilter: 'button.clear-filters',
loadMore: 'button.load-more',
filterContainer: '.filters',
@@ -63,15 +63,17 @@
};
this.ui = window.uiFromSelectors(this.elements);
- this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]');
+
+ this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
- if (this.ui.content.length > 0) {
+ 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,
@@ -79,6 +81,8 @@
} else {
this.taxonomies = [];
}
+
+
}
async initTaxonomies() {
@@ -89,24 +93,39 @@
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);
});
- // Make this non-blocking
- setTimeout(() => {
- this.selector.batchFetchTaxonomies();
- this.selector.isInitializing = false;
- }, 0);
+ this.selector.isInitializing = false;
this.selector.subscribe((event, data) => {
if (event === 'selected-terms') this.handleTaxonomyChange(data);
});
}
+ 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;
@@ -161,13 +180,15 @@
this.syncUIToFilters();
}
syncUIToFilters() {
- // 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;
- }
- });
+ 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;
+ }
+ });
+ }
// Update content-specific visibility
this.updateContentFor(this.filters.content);
@@ -177,10 +198,10 @@
}
initStore() {
- this.addPlaceholders();
- this.store = window.jvbStore.register(
+ const store = window.jvbStore.register(
'feed',
{
+ storeName: 'feed',
endpoint: 'feed',
keyPath: 'id',
indexes: [
@@ -197,6 +218,7 @@
delayFetch: true
}
);
+ this.store = store.feed;
this.store.subscribe((event, data) => {
switch (event) {
@@ -266,7 +288,7 @@
this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
}
});
- if (hasTaxonomy) {
+ 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) {
@@ -317,7 +339,6 @@
let items = this.store.getFiltered();
if (this.store.filters['page'] === 1) {
window.removeChildren(this.ui.grid);
- this.addPlaceholders();
}
if (items.length === 0) {
@@ -344,9 +365,6 @@
this.removePlaceholders();
this.ui.grid.append(fragment);
- // Observe images after adding to DOM
- this.observeImages(this.ui.grid);
-
if (this.config.gallery) {
this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
}
@@ -362,8 +380,12 @@
this.a11y.announceItems(0, this.store.filters['page'] >1, false);
}
- this.ui.filters.match.hidden = window.isEmptyObject(this.taxonomyFilters);
- this.ui.clearFilter.hidden = window.isEmptyObject(this.taxonomyFilters);
+ 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;
+ }
}
/**
@@ -371,92 +393,225 @@
* @param {object} item
*/
createItemElement(item) {
- let template = window.getTemplate('feed-item');
+ let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
if (Object.hasOwn(template.dataset, 'timeline')) {
return this.createTimelineElement(item, template);
}
+ //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;
}
+ splitIDs(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) {
+ console.log('Item has no images, or the images object is empty');
+ return false;
+ }
+ let values = this.splitIDs(value);
+ values.forEach(v => {
+ console.log('Checking id: ', v);
+ if (Object.keys(item.images).includes(parseInt(v))) {
+ console.log('Item.images does not have id');
+ return true;
+ }
+ });
+ return false;
+ }
+ formatImageFields(element, value, item) {
+ console.log('Formatting image Field: ', element);
+ console.log('value: ', value);
+ console.log('item: ', 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 image = element.querySelector('img');
+ if (!image) return;
+ value.forEach(imgID => {
+ let img = image.cloneNode(true);
+ this.formatImageField(img, imgID, item);
+ element.append(img);
+ });
+ image.remove();
+ } else {
+ console.log(element.tagName);
+ if (element.tagName !== 'IMG') {
+ element = element.querySelector('img');
+ if (!element) return;
+ }
+ this.formatImageField(element, value[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']
+ ]
+ }
+ 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;
+ }
+ formatTaxonomyField(element, item, field, value) {
+ if (element.tagName !== 'UL' || !element.querySelector('li')) return;
+
+ let values = this.splitIDs(value);
+ let listItem = element.querySelector('li');
+ for (let termID of values) {
+ let term = item.taxonomies[field][termID]??false;
+ if (!term) continue;
+ let termItem = listItem.cloneNode(true);
+ let link = termItem.querySelector('a');
+ if (!link) continue;
+
+ [
+ link.href,
+ link.title,
+ link.textContent
+ ] = [
+ term.url,
+ `See more ${term.title}`,
+ term.title
+ ];
+ element.append(termItem);
+ }
+ listItem.remove();
+ }
+ isTimeField(el) {
+ return el.tagName === 'TIME' || el.querySelector('time') !== null;
+ }
+ formatTimeField(element, value) {
+ if (element.tagName !== 'TIME') {
+ element = element.querySelector('time');
+ if (!element) return;
+ }
+ element.setAttribute('datetime', value);
+ element.textContent = window.formatTimeAgo(value);
+ }
+ formatField(element, value) {
+ element.textContent = 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);
+ }
+ }
+ }
let [
- main,
- link,
- beforeImg,
- afterImg,
- afterText,
+ afterEl,
+ number,
started,
- lastTreated,
- total,
- termList,
- timeline
+ last
] = [
- template,
- template.querySelector('a'),
- template.querySelector('img.before'),
- template.querySelector('img.after'),
- template.querySelector('summary span:last-of-type'),
- template.querySelector('p.started time'),
- template.querySelector('p.updated time'),
- template.querySelector('p.total b'),
- template.querySelector('.term-list'),
- Object.values(item.fields.order)
+ template.querySelector('span.after-text'),
+ template.querySelector('[data-field="number"] b'),
+ template.querySelector('[data-field="started"] time'),
+ template.querySelector('[data-field="updated"] time')
];
- let numberTreatments = timeline.length - 1;
- let beforeImgData = item.images[timeline[0]['post_thumbnail']];
- let afterImgData = item.images[timeline[numberTreatments]['post_thumbnail']];
+ if (afterEl) {
+ afterEl.textContent = `After ${item.fields.number} Tx`;
+ }
+ if (number) {
+ number.textContent = item.fields.number;
+ }
+ if (started) {
+ this.formatTimeField(started, item.fields.timeline[0]['post_date']);
+ }
+ if (last) {
+ this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']);
+ }
- [
- main.dataset.id,
- link.href,
- beforeImg.src,
- beforeImg.dataset.small,
- beforeImg.dataset.medium,
- afterImg.src,
- afterImg.dataset.small,
- afterImg.dataset.medium,
- afterText.textContent,
- started.textContent,
- lastTreated.textContent,
- total.textContent
- ] = [
- item.id,
- item.url,
- beforeImgData['tiny'],
- beforeImgData.small,
- beforeImgData.medium,
- afterImgData['tiny'],
- afterImgData.small,
- afterImgData.medium,
- `${afterText.textContent} ${numberTreatments} Tx`,
- timeline[0].date??item.date,
- timeline[numberTreatments].date??'',
- `${numberTreatments} Treatments`
- ];
return template;
}
+ removePlaceholders() {
+ const placeholders = this.ui.grid.querySelectorAll('.placeholder');
+ if (placeholders.length > 0) {
+ placeholders.forEach(p => p.remove());
+ }
+ }
+
+
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.length > 0) {
+ 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.appendChild(icon);
- this.ui.grid.appendChild(template);
+ template.append(icon);
+ fragment.append(template);
}
- }
-
- removePlaceholders() {
- if (this.ui.grid.querySelector('.placeholder')) {
- window.removeChildren(this.ui.grid);
- }
+ this.ui.grid.append(fragment);
}
@@ -543,13 +698,21 @@
}
if ('ResizeObserver' in window) {
- this.resizeObserver = new ResizeObserver(window.debounce(() => {
- this.updateImageSizes();
- }, 250));
+ this.resizeObserver = new ResizeObserver(() => {
+ window.debouncer.schedule(
+ 'feed-update-images',
+ () => this.updateImageSizes(),
+ 250
+ );
+ });
} else {
- window.addEventListener('resize', window.debounce(()=> {
- this.updateImageSizes();
- }, 250));
+ window.addEventListener('resize', () => {
+ window.debouncer.schedule(
+ 'feed-update-images',
+ () => this.updateImageSizes(),
+ 250
+ );
+ });
}
window.addEventListener('popstate', this.popStateHandler);
@@ -557,35 +720,6 @@
document.addEventListener('change', this.changeHandler);
}
- loadImage(img) {
- const src = this.getAppropriateImageSize(img);
- if (src && src !== img.src) {
- img.src = src;
- img.dataset.loaded = 'true';
- }
- }
-
- getAppropriateImageSize(img) {
- const width = window.innerWidth;
-
- if (width < 768 && img.dataset.small) {
- return img.dataset.small;
- } else if (img.dataset.medium) {
- return img.dataset.medium;
- }
-
- return img.src; // Fallback to current src
- }
-
- observeImages(container) {
- const images = container.querySelectorAll('img[data-small], img[data-medium]');
- images.forEach(img => {
- if (!img.dataset.loaded) {
- this.imageObserver.observe(img);
- }
- });
- }
-
handlePopState(e) {
if (e.state?.filters) {
if (this.processURLFilters()) {
@@ -654,6 +788,39 @@
}
}
-document.addEventListener('DOMContentLoaded', function() {
- window.feedBlock = new FeedBlock();
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe(event => {
+ if (event === 'auth-loaded') {
+ 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