class FeedBlock {
|
constructor() {
|
this.cache = window.jvbCache;
|
this.a11y = window.jvbA11y;
|
this.loading = window.jvbLoading;
|
this.error = window.jvbError;
|
|
|
this.container = document.querySelector('section.feed-block');
|
if (!this.container) {
|
return;
|
}
|
|
this.openGallery = false;
|
|
this.initElements();
|
this.addPlaceholders();
|
this.config = {
|
api: feedSettings.apiUrl,
|
nonce: feedSettings.nonce,
|
user: jvbSettings.currentUser || null,
|
source: '',
|
context: '',
|
highlight: null,
|
gallery: false,
|
showAuthor: true,
|
showDate: false,
|
view: localStorage.getItem('feedViewMode') || 'grid',
|
... this.container.dataset
|
};
|
this.taxonomies = {};
|
this.rendered = {};
|
|
this.feed = {
|
imageLoadThreshold: 5,
|
lazyLoadOffset: '100px',
|
gallery: [],
|
loaded: 0,
|
intsersectionObserver: null,
|
templates: new Map()
|
};
|
|
this.isLoading = false;
|
this.hasMore = true;
|
this.retries = {
|
count: 0,
|
max: 3,
|
delay: 1000
|
};
|
this.page = 1;
|
this.order = 'DESC';
|
this.orderby = 'date';
|
|
this.gallery = (this.config.gallery) ? new window.jvbGallery(document.querySelector('dialog.gallery'), {
|
imageWrapper: '.item',
|
loadMore: ()=>this.fetchFeed.bind(this)
|
}) : false;
|
this.initListeners();
|
if (this.page === 1) {
|
this.processURLFilters();
|
} else {
|
this.updateFilters();
|
}
|
|
}
|
initElements() {
|
this.filterSelector = 'form.feed-filters';
|
this.filterForm = this.container.querySelector(this.filterSelector);
|
this.grid = this.container.querySelector('.item-grid');
|
this.loadMore = this.container.querySelector('.load-more');
|
this.filterControls = this.container.querySelector('.filter-actions');
|
this.contentTypes = Array.from(this.filterForm.querySelectorAll('input[name="content"]')).map(
|
content => {
|
return content.value;
|
});
|
this.selectedTerms = this.container.querySelector('.selected-items-section .selected-items');
|
}
|
|
initListeners() {
|
window.addEventListener('popstate', this.handlePopState.bind(this));
|
document.addEventListener('click', this.handleClick.bind(this));
|
document.addEventListener('change', this.handleChange.bind(this));
|
|
|
// Intersection observer for lazy loading
|
if ('IntersectionObserver' in window) {
|
this.imageObserver = new IntersectionObserver(entries => {
|
entries.forEach(entry => {
|
if (entry.isIntersecting) {
|
this.loadImage(entry.target);
|
this.imageObserver.unobserve(entry.target);
|
}
|
});
|
}, {
|
rootMargin: '100px',
|
threshold: 0.1
|
});
|
}
|
// Resize observer for responsive images
|
if ('ResizeObserver' in window) {
|
this.resizeObserver = new ResizeObserver(window.debounce(() => {
|
this.updateImageSizes();
|
}, 250));
|
|
// Observe the container
|
this.resizeObserver.observe(this.container);
|
} else {
|
// Fallback to window resize
|
window.addEventListener('resize', window.debounce(() => {
|
this.updateImageSizes();
|
}, 250));
|
}
|
|
this.taxonomies = {};
|
this.container.querySelectorAll('.jvb-selector:not([hidden])').forEach(selector => {
|
let taxonomy = selector.dataset.taxonomy;
|
if (!Object.hasOwn(this.taxonomies, taxonomy)) {
|
this.taxonomies[taxonomy] = new window.jvbTaxonomySelector(
|
selector,
|
{
|
multiple: true,
|
feed: true,
|
selected: {},
|
onClose: () => this.setSelectedTerms(taxonomy),
|
}
|
);
|
}
|
});
|
}
|
|
|
/**
|
* Handle browser history navigation
|
*/
|
handlePopState(e) {
|
if (e.state && e.state.filters) {
|
if(this.processURLFilters()){
|
// Load items with updated filters
|
this.resetPage();
|
this.fetchFeed();
|
|
// Announce to screen readers
|
this.a11y.announce('Feed filters updated from browser history.');
|
}
|
}
|
}
|
|
processURLFilters() {
|
const params = new URLSearchParams(window.location.search);
|
//No parameters to process
|
if (!params.toString()) {
|
this.updateFilters();
|
return;
|
}
|
|
let filters = ['content', 'order', 'orderby', 'favourites','match'];
|
|
filters.forEach(filter => {
|
let value = params.get('f_'+filter);
|
params.delete('f_'+filter);
|
if (value && this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`)) {
|
this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`).checked = true;
|
}
|
});
|
|
let unprocessed = {};
|
for (var [key, value] of Object.entries(Object.fromEntries(params))) {
|
|
key = key.replace('f_','');
|
if (this.contentTypes.includes(key)) {
|
this.openGallery = value;
|
} else {
|
this.taxonomies[key].addTermsFromURL(value);
|
this.setSelectedTerms(key);
|
}
|
}
|
|
this.updateFilters();
|
}
|
|
handleClick(e) {
|
if (e.target.classList.contains('load-more') || e.target.closest('.load-more')) {
|
this.fetchFeed(false);
|
e.target.disabled = true;
|
} else if (e.target.classList.contains('clear-filters') || e.target.closest('.clear-filters')) {
|
this.resetFilters();
|
} else if (this.config.gallery && e.target.closest('.feed-image')) {
|
this.gallery.handleGalleryOpen(e);
|
} else if (e.target.classList.contains('.remove-item') || e.target.closest('.remove-item')) {
|
let tag = e.target.closest('.selected-item');
|
let taxonomy = tag.dataset.taxonomy;
|
this.taxonomies[taxonomy].removeSelectedTerm(tag.dataset.id);
|
this.setSelectedTerms(taxonomy);
|
this.updateFilters();
|
}
|
}
|
handleChange(e) {
|
if (e.target.closest(this.filterSelector)) {
|
this.resetPage();
|
window.removeChildren(this.grid);
|
this.addPlaceholders();
|
//update filters
|
this.updateFilters();
|
}
|
}
|
|
updateFilters() {
|
this.page = 1;
|
const params = new URLSearchParams(window.location.search);
|
|
let filters = Object.fromEntries(new FormData(this.filterForm));
|
|
let contents = [];
|
for (let [key, value] of Object.entries(filters)) {
|
let set = false;
|
switch (key) {
|
case 'content':
|
if (value !== this.contentTypes[0]) {
|
set = true;
|
} else {
|
params.delete('f_'+key);
|
}
|
break;
|
case 'orderby':
|
if (value !== 'date') {
|
set = true;
|
}
|
break;
|
case 'order':
|
if (value !== 'desc') {
|
set = true;
|
}
|
break;
|
default:
|
set = true;
|
}
|
if (!set) {
|
params.delete('f_'+key);
|
}
|
|
|
if (set && value !== false && value !== '') {
|
params.set('f_'+key, value);
|
}
|
if (value !== '') {
|
contents.push(value);
|
}
|
|
const newURL = `${window.location.pathname}?${params.toString()}`;
|
history.pushState(filters, '', newURL);
|
|
}
|
|
this.filters = filters;
|
this.updateContentFor(filters.content);
|
|
this.updateFilterControls();
|
|
this.loading.setContent(contents);
|
this.fetchFeed(true);
|
}
|
|
updateFilterControls() {
|
this.filterControls.hidden = this.selectedTerms.children.length < 2;
|
}
|
|
/**
|
* Toggles taxonomy selectors and certain order/orderby options
|
* depending on current content
|
* @param content
|
*/
|
updateContentFor(content) {
|
this.filterForm.querySelectorAll('.jvb-selector').forEach(tax => {
|
let hasContent = tax.dataset.for.includes(content);
|
tax.hidden = !hasContent;
|
if (!hasContent) {
|
let t = tax.dataset.taxonomy;
|
this.clearSelectedTerms(t);
|
}
|
});
|
this.filterForm.querySelectorAll('input[data-for]').forEach(toggle => {
|
toggle.hidden = !toggle.dataset.for.includes(content);
|
});
|
this.filterForm.querySelectorAll('input[name="order"]').forEach(order => {
|
order.hidden = this.filters.order === 'random';
|
});
|
}
|
|
clearSelectedTerms(taxonomy) {
|
this.filterForm.querySelector(`input[name="${taxonomy}"]`).value = '';
|
if (Object.hasOwn(this.taxonomies, taxonomy)) {
|
this.taxonomies[taxonomy].selectedItems = {};
|
}
|
}
|
|
setSelectedTerms(taxonomy) {
|
let input = this.filterForm.querySelector(`input[name="${taxonomy}"]`);
|
input.value = '';
|
let selected = this.taxonomies[taxonomy].selectedTerms;
|
if (!window.isEmptyObject(selected)) {
|
let ids = Object.keys(selected);
|
input.value = ids.join(',');
|
}
|
this.updateFilters();
|
}
|
|
|
nextPage() {
|
if (this.hasMore) {
|
this.page++;
|
}
|
}
|
resetPage() {
|
this.page = 1;
|
this.hasMore = true;
|
}
|
resetState() {
|
this.resetPage(true);
|
this.isLoading = false;
|
this.retries = {
|
count: 0,
|
max: 3,
|
delay: 1000
|
};
|
}
|
|
resetFilters() {
|
this.filterForm.reset();
|
//check the first content
|
this.filterForm.querySelector('input[name="content"]').checked = true;
|
this.filterForm.querySelector('input[name="orderby"][value="date"]').checked = true;
|
this.page = 1;
|
this.updateFilters();
|
}
|
|
|
buildFilterRequest() {
|
|
let filters = {};
|
|
for (let [filter, value] of Object.entries(this.filters)) {
|
if (value !== false && value !== '') {
|
filters[filter] = value;
|
}
|
}
|
filters.page = parseInt(this.page);
|
if (this.container.dataset.context) {
|
filters.context = this.container.dataset.context;
|
}
|
if (this.container.dataset.source) {
|
filters.source = this.container.dataset.source;
|
}
|
return new URLSearchParams(filters).toString();
|
}
|
|
async fetchFeed(reset = false, force = false) {
|
if (this.isLoading) {
|
return false;
|
}
|
this.loading.showLoading(this.filters);
|
try {
|
if (this.page === 1) {
|
window.removeChildren(this.grid);
|
this.addPlaceholders();
|
}
|
|
const data = await this.cache.fetchWithCache(
|
`${this.config.api}feed?${this.buildFilterRequest()}`,
|
{
|
method: 'GET',
|
},
|
{
|
context: 'feed',
|
forceRefresh: true
|
// forceRefresh: force
|
}
|
);
|
|
//Handle empty results
|
if (!data || !data.items || data.items.length === 0) {
|
if (this.page === 1) {
|
this.showEmptyState();
|
}
|
this.hasMore = false;
|
return false;
|
} else {
|
this.hasMore = data['has_more'];
|
|
this.renderItems(data.items, this.page > 1);
|
|
if (this.hasMore) {
|
this.nextPage();
|
}
|
return true;
|
}
|
} catch (error) {
|
this.handleError(error);
|
} finally {
|
this.loading.hideLoading();
|
if (this.openGallery !== false) {
|
this.gallery.openWhenReady = this.openGallery;
|
this.openGallery = false;
|
}
|
this.loadMore.disabled = false;
|
this.loadMore.hidden = !this.hasMore;
|
}
|
}
|
|
removePlaceholders() {
|
if (this.grid.querySelector('.placeholder')) {
|
window.removeChildren(this.grid);
|
}
|
}
|
showEmptyState() {
|
window.removeChildren(this.grid);
|
let template = window.getTemplate('emptyState');
|
let isFavourite = Object.hasOwn(this.filters, 'favourites') && this.filters.favourites === true;
|
if (isFavourite) {
|
[
|
template.querySelector('h3').textContent,
|
template.querySelector('p:first-of-type').textContent,
|
template.querySelector('p:last-of-type').textContent,
|
] = [
|
'♡ BLANK CANVAS ♡',
|
'You haven\'t fallen in love with any pieces... yet!',
|
'Hit that heart icon when something stops your scroll — your dream collection is waiting to start.'
|
];
|
}
|
this.grid.append(template);
|
this.a11y.announceEmpty(isFavourite);
|
|
}
|
handleError(error){
|
return this.error.handleApiError(
|
error,
|
{
|
component: 'Feed Block',
|
action: 'loaditems'
|
},
|
() => this.fetchFeed()
|
);
|
}
|
|
addPlaceholders() {
|
let total = this.contentTypes.length - 1;
|
for (let i = 0; i < 9; i++) {
|
let template = window.getTemplate('placeholderTemplate');
|
let rand = Math.floor(Math.random()*total+1);
|
let icon = window.getIcon(this.contentTypes[rand]).cloneNode(true);
|
|
template.append(icon);
|
this.grid.append(template);
|
}
|
}
|
renderItems(items, append = false) {
|
//Clear the grid if we aren't appending
|
if (!append) {
|
window.removeChildren(this.grid);
|
this.addPlaceholders();
|
}
|
|
|
//Bail early if no items
|
if (items.length === 0) {
|
this.a11y.announceUpdate(0, append);
|
return;
|
}
|
|
//Use DocumentFragment for better performance
|
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);
|
|
this.imageObserver.observe(element);
|
}
|
|
if (endIndex < items.length) {
|
requestAnimationFrame(() => {
|
processBatch(endIndex);
|
});
|
} else {
|
this.removePlaceholders();
|
//all batches are processed, append fragment
|
this.grid.appendChild(fragment);
|
if (this.config.gallery) {
|
this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
|
}
|
this.a11y.makeNavigable(this.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
|
this.a11y.announceItems(items.length, append, this.hasMore);
|
}
|
};
|
|
if (items.length > 0) {
|
processBatch(0);
|
} else {
|
this.a11y.announceUpdate(0, append);
|
}
|
}
|
|
/**
|
* Creates a feed-item. Used by RenderItems
|
*/
|
createItemElement(item) {
|
if(!this.rendered[item.icon]) {
|
this.rendered[item.icon] = new Map();
|
}
|
if (this.rendered[item.icon].has(item.id)) {
|
return this.rendered[item.icon].get(item.id);
|
}
|
|
const favourited = window.isFavourited(item.icon, item.id)??false;
|
const template = window.getTemplate('feed-item');
|
|
template.id = `${item.icon}-${item.id}`;
|
template.dataset.id = item.id;
|
template.classList.add(item.icon);
|
|
if (item['umami_view']) {
|
this.buildUmamiData(template, item['umami_view']);
|
}
|
|
let favouriteButton = template.querySelector('button.favourite');
|
[
|
favouriteButton.dataset.id,
|
favouriteButton.dataset.type,
|
favouriteButton.dataset.artist,
|
favouriteButton.title
|
] = [
|
item.id,
|
item.icon,
|
item['user_id'],
|
(favourited) ? 'Remove from Favourites' : 'Add to Favourites'
|
];
|
|
let order = item.order;
|
let single = template.querySelector('.item');
|
let list = template.querySelector('.item-list');
|
let img = template.querySelector('.feed-images');
|
let summary = template.querySelector('summary');
|
let info = template.querySelector('.item-info');
|
|
for (let [index, id] of Object.entries(order)) {
|
let target;
|
let config = item[id];
|
if (id === 'title') {
|
target = template.querySelector('h3 a');
|
if (item.title !== '') {
|
[
|
target.textContent,
|
target.href,
|
target.url
|
] = [
|
item.title,
|
item.url,
|
`Learn more about this ${item.icon}`
|
];
|
if (item.icon !== '') {
|
target.closest('h3').prepend(window.getIcon(item.icon));
|
}
|
if (item.umami_click) {
|
this.buildUmamiData(target, item.umami_click);
|
}
|
} else {
|
target.remove();
|
}
|
} else if (Object.hasOwn(config, 'terms')) {
|
//Taxonomy list
|
if (config.terms.length === 0) {
|
continue;
|
}
|
let taxonomy = list.cloneNode(true);
|
let label = taxonomy.querySelector('.label');
|
let termList = taxonomy.querySelector('ul');
|
let listItem = taxonomy.querySelector('li');
|
|
if (config.label) {
|
label.textContent = config.label;
|
}
|
if (config.icon) {
|
label.prepend(window.getIcon(config.icon));
|
}
|
if (!config.label && !config.icon){
|
label.remove();
|
}
|
|
config.terms.forEach(term => {
|
let termItem = listItem.cloneNode(true);
|
let link = termItem.querySelector('a');
|
[
|
link.href,
|
link.title,
|
link.textContent
|
] = [
|
term.url,
|
`Learn more about ${term.title}`,
|
term.title
|
];
|
if (term.umami_click.length > 0) {
|
this.buildUmamiData(link, term.umami_click);
|
}
|
termList.append(termItem);
|
});
|
|
listItem.remove();
|
info.appendChild(taxonomy);
|
} else if (Object.hasOwn(config, 'value') && config.value !== '') {
|
let itemInfo = single.cloneNode(true);
|
let label = itemInfo.querySelector('.label');
|
let link = itemInfo.querySelector('a');
|
let p = itemInfo.querySelector('p');
|
if (Object.hasOwn(config, 'label')) {
|
label.textContent = config.label;
|
}
|
if (Object.hasOwn(config, 'icon')) {
|
label.prepend(window.getIcon(config.icon));
|
}
|
if (!Object.hasOwn(config, 'icon') && !Object.hasOwn(config, 'label')) {
|
label.remove();
|
}
|
if (Object.hasOwn(config, 'url')) {
|
p.remove();
|
[
|
link.textContent,
|
link.href,
|
link.title
|
] = [
|
config.value,
|
config.url,
|
`Learn more about ${config.value}`
|
];
|
} else {
|
link.remove();
|
p.textContent = config.value;
|
}
|
info.appendChild(itemInfo);
|
} else if (id === 'image') {
|
let images = summary.querySelector('.feed-images');
|
let img = images.querySelector('a');
|
|
let main = img.cloneNode(true);
|
if (!this.config.gallery) {
|
main.href = item.url;
|
}
|
|
main.classList.add('feed-image');
|
this.buildImageData(main.querySelector('img'), item.image);
|
images.append(main);
|
|
if (item.content?.length > 0) {
|
images.classList.add('multi');
|
item.content.forEach(c => {
|
let image = img.cloneNode(true);
|
if (!this.config.gallery) {
|
image.href = c.url;
|
}
|
let itemImg = image.querySelector('img');
|
itemImg.src = c.image.small;
|
itemImg.alt = c.image.alt;
|
images.append(image);
|
});
|
}
|
img.remove();
|
}
|
}
|
single.remove();
|
list.remove();
|
|
this.rendered[item.icon].set(item.id, template);
|
|
return template;
|
}
|
|
buildImageData(img, data){
|
if (typeof data.tiny !== 'string') {
|
return;
|
}
|
[
|
img.src,
|
img.dataset.small,
|
img.dataset.medium,
|
img.dataset.large,
|
img.alt
|
] =
|
[
|
data.tiny,
|
data.small,
|
data.medium,
|
data.large,
|
data.alt
|
];
|
}
|
|
buildUmamiData(item, data){
|
for(let [key, value] of Object.entries(data)){
|
item.dataset[key] = value;
|
}
|
}
|
|
/**
|
* Load Image, used by renderItems
|
* @param element
|
*/
|
loadImage(element) {
|
const img = element.querySelector('img');
|
if (!img) return;
|
const size = this.getImageSize();
|
|
img.src = img.dataset[size] || img.dataset.src;
|
element.setAttribute('data-loaded', 'true');
|
}
|
|
/**
|
* Updates the image size according to screen size
|
*/
|
updateImageSizes() {
|
const size = this.getImageSize();
|
const items = this.grid.querySelectorAll('.item');
|
items.forEach(item => {
|
const img = item.querySelector('img');
|
if (img && img.dataset[size] && img.src !== img.dataset[size]) {
|
img.src = img.dataset[size];
|
}
|
});
|
}
|
/**
|
* Get image size based on screen width
|
*/
|
getImageSize() {
|
const width = window.innerWidth;
|
if (width > 1024) return 'medium';
|
if (width > 500) return 'medium';
|
return 'small';
|
}
|
|
}
|
document.addEventListener('DOMContentLoaded', () => {
|
window.feedBlock = new FeedBlock();
|
});
|