From 56a9a1ccf764ff7a6af8f8a2292cb07443cb4aa7 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 28 May 2026 18:19:57 +0000
Subject: [PATCH] =New Gitbit setpu
---
src/feed/view.js | 1318 ++++++++++++++++++++++++++++++-----------------------------
1 files changed, 660 insertions(+), 658 deletions(-)
diff --git a/src/feed/view.js b/src/feed/view.js
index 29859f9..894f85c 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -1,745 +1,747 @@
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;
- }
+ if(!this.container) return;
- this.openGallery = false;
+ this.a11y = window.jvbA11y;
+ this.error = window.jvbError;
+ this.cache = new window.jvbCache('feed');
+ this.templates = window.jvbTemplates;
- 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',
+ view: this.cache.get('feedView') || '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();
- }
-
+ this.init();
}
+ init() {
+ this.initElements();
+ this.defineTemplates();
+ this.initListeners();
+ this.initFilters();
+
+ if ('requestIdleCallback' in window) {
+ requestIdleCallback(() => {
+ this.initStore();
+ this.initTaxonomies();
+
+ this.processCachedFilters();
+ this.processURLFilters();
+ this.updateFilterUI();
+ this.initGallery();
+ }, { timeout: 2000 });
+ } else {
+ setTimeout(() => {
+ this.initStore();
+ this.initTaxonomies();
+
+ this.processCachedFilters();
+ this.processURLFilters();
+ this.updateFilterUI();
+ this.initGallery();
+ }, 100);
+ }
+ }
+
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');
+ this.selectors = {
+ filterTrigger: '[data-filter]',
+ filters: {
+ actions: '.filter-actions .toggle-text',
+ container: '.all-filters',
+ content: '[data-filter="content"]',
+ orderby: '[data-filter="orderby"]',
+ order: '[data-filter="order"]',
+ match: '[data-filter="match"]',
+ favourites: '[data-filter="favourites"]',
+ taxonomy: '[data-filter^="taxonomy"]',
+ },
+ 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.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.taxonomies = false;
+ this.ui.orderbyWrap = this.ui.filters.container.querySelector('[data-for-order]');
+ if (this.ui.orderbyWrap.length === 0) this.ui.orderbyWrap = false;
+ this.ui.order = this.ui.filters.container.querySelectorAll('[data-filter="order"]');
+ if (this.ui.order.length === 0) this.ui.order = false;
+ this.ui.orderby = this.ui.filters.container.querySelectorAll('[data-filter="orderby"]');
+ if (this.ui.orderby.length === 0) this.ui.orderby = false;
+
+ this.orderbyFilters = (this.ui.orderby)
+ ? Array.from(this.ui.orderby).map(o => o.value)
+ : [];
+
+ this.contentTypes = (this.ui.content)
+ ? Array.from(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)
+ : [];
}
initListeners() {
- window.addEventListener('popstate', this.handlePopState.bind(this));
- document.addEventListener('click', this.handleClick.bind(this));
- document.addEventListener('change', this.handleChange.bind(this));
+ this.popStateHandler = this.handlePopState.bind(this);
+ this.clickHandler = this.handleClick.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
+ window.addEventListener('popstate', this.popStateHandler);
+ document.addEventListener('click', this.clickHandler);
+ document.addEventListener('change', this.changeHandler);
+ }
- // 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);
+ initFilters() {
+ this.allowedFilters = ['content', 'order', 'orderby', 'favourites', 'match'];
+ let defaults = {
+ content: this.contentTypes[0],
+ orderby: 'date',
+ order: 'desc',
+ page: 1,
+ };
+ if (this.config.context) defaults.context = this.config.context;
+ if (this.config.source) defaults.source = this.config.source;
+
+ this.filters = defaults;
+ this.defaults = {...defaults};
+ }
+ 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;
+ }
}
- });
- }, {
- 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),
- }
- );
+ 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);
+ }
+ });
+ }
}
- });
+ }
}
-
- /**
- * 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.');
+ if (e.state?.filters) {
+ if (this.processURLFilters()) {
+ this.store.setFilters(this.filters);
+ 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();
+ 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) {
- 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) {
+ 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':
- if (value !== this.contentTypes[0]) {
- set = true;
- } else {
- params.delete('f_'+key);
- }
+ this.updateContentFor(target.value);
break;
case 'orderby':
- if (value !== 'date') {
- set = true;
- }
+ this.updateOrderOptions(target.value);
break;
- case 'order':
- if (value !== 'desc') {
- set = true;
- }
- break;
- default:
- set = true;
}
- if (!set) {
- params.delete('f_'+key);
+ }
+ }
+
+ 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(Array.from(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();
-
- 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);
-
+ // 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.filters = filters;
- this.updateContentFor(filters.content);
+ this.resetFilters({
+ taxonomy: Object.keys(this.taxFilters).length > 0
+ ? this.taxFilters
+ : null
+ });
+ }
- this.updateFilterControls();
-
- this.loading.setContent(contents);
- this.fetchFeed(true);
+ updateContentFor(content) {
+ let checkIt = [
+ this.ui.taxonomies,
+ this.ui.orderby
+ ];
+ 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.orderbyWrap) {
+ let options = this.ui.orderbyWrap.dataset.forOrder.split(',')??[];
+ this.ui.orderbyWrap.hidden = !options.includes(order);
+ }
}
updateFilterControls() {
- this.filterControls.hidden = this.selectedTerms.children.length < 2;
+ 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;
+ }
}
- /**
- * 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);
+ 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;
+
}
});
- this.filterForm.querySelectorAll('input[data-for]').forEach(toggle => {
- toggle.hidden = !toggle.dataset.for.includes(content);
+ }
+ 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.filterForm.querySelectorAll('input[name="order"]').forEach(order => {
- order.hidden = this.filters.order === 'random';
+ this.updateFilterControls();
+ }
+ getTaxonomyIcon(taxonomy) {
+ let iconButton = Array.from(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.source}_${this.config.context}_${filter}`);
+ if (cached && cached !== this.filters[filter]) {
+ this.filters[filter] = cached;
+ }
});
}
- clearSelectedTerms(taxonomy) {
- this.filterForm.querySelector(`input[name="${taxonomy}"]`).value = '';
- if (Object.hasOwn(this.taxonomies, taxonomy)) {
- this.taxonomies[taxonomy].selectedItems = {};
+ 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;
}
- 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();
- }
+ 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]);
+ }
+ });
-
- 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;
+ for (let [tax, terms] of Object.entries(this.taxFilters)) {
+ if (terms.length > 0) {
+ params.set(`f_tax_${tax}`, terms.join(','));
}
}
- filters.page = parseInt(this.page);
- if (this.container.dataset.context) {
- filters.context = this.container.dataset.context;
+
+ 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);
}
- 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();
- }
+ saveToCacheFilters() {
+ Object.keys(this.store.filters).forEach(filter => {
+ const cacheKey = `${this.config.source}_${this.config.context}_${filter}`;
- 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;
+ if (this.store.filters[filter] !== this.defaults[filter]) {
+ this.cache.set(cacheKey, this.store.filters[filter]);
} else {
- this.hasMore = data['has_more'];
+ this.cache.remove(cacheKey);
+ }
+ });
- this.renderItems(data.items, this.page > 1);
+ const taxCacheKey = `${this.config.source}_${this.config.context}_taxonomy`;
+ if (Object.keys(this.taxFilters).length > 0) {
+ this.cache.set(taxCacheKey, this.taxFilters);
+ } else {
+ this.cache.remove(taxCacheKey);
+ }
+ }
- if (this.hasMore) {
+ 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();
}
- return true;
+ });
+ }
+ }
+ initStore() {
+ let extraOrderby = this.orderbyFilters.filter(v => !['date','modified','title','random'].includes(v));
+ let extraIndexes = [];
+ extraOrderby.forEach(orderby =>{
+ extraIndexes.push({name:orderby, keyPath: orderby});
+ });
+ const store = window.jvbStore.register(
+ 'feed',
+ {
+ storeName: 'feed',
+ endpoint: 'feed',
+ keyPath: 'id',
+ indexes: [
+ { name: 'content', keyPath: 'content'},
+ { name: 'taxonomy', keyPath: 'taxonomy'},
+ { name: 'user', keyPath: 'user'},
+ { 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',
}
- } catch (error) {
- this.handleError(error);
- } finally {
- this.loading.hideLoading();
- if (this.openGallery !== false) {
- this.gallery.openWhenReady = this.openGallery;
- this.openGallery = false;
+ );
+
+ this.store = store.feed;
+
+ this.store.subscribe((event, data) => {
+ switch (event) {
+ case 'data-loaded':
+ 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;
}
- this.loadMore.disabled = false;
- this.loadMore.hidden = !this.hasMore;
+ });
+ }
+
+ isFirstPage() {
+ return 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.showEmptyState();
+ this.a11y.announceItems(0, this.isFirstPage());
+ } 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);
+ },
+ 5
+ ).then(()=>{});
+ }
+
+ this.updateFilterControls();
+ }
+
+ showEmptyState() {
+ window.removeChildren(this.ui.grid);
+ this.ui.grid.append(this.templates.create('emptyState'));
+ }
+
+ createItemElement(item) {
+ if (typeof item !== 'object') {
+ item = this.store.get(item);
+ if (!item) return;
+ }
+ return this.templates.create(`feedItem${window.uppercaseFirst(item.content)}`, item);
+ }
+ 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) {
+ return false;
+ }
+ let values = this.splitIDs(value);
+
+ return values.some(v =>
+ Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v))
+ );
+ }
+ formatImageFields(element, value, item) {
+ 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;
+ values.forEach(imgID => {
+ let img = image.cloneNode(true);
+ this.formatImageField(img, imgID, item);
+ element.append(img);
+ });
+ image.remove();
+ } else {
+ if (element.tagName !== 'IMG') {
+ element = element.querySelector('img');
+ if (!element) return;
+ }
+ this.formatImageField(element, values[0], item);
+ }
+ }
+ 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;
+ }
+
+ 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;
+ if (!term) continue;
+ let termItem = listItem.cloneNode(true);
+ let link = termItem.querySelector('a');
+ if (!link) continue;
+
+ let title = window.decodeHTMLEntities(term.title);
+
+ [
+ link.href,
+ link.title,
+ link.textContent
+ ] = [
+ term.url,
+ `See more ${title}`,
+ 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, 'F Y');
+ }
+ formatField(element, value) {
+ element.textContent = window.decodeHTMLEntities(value);
+ }
+
+ addTimelineElements(item, template) {
+ let [
+ afterEl,
+ number,
+ started,
+ last
+ ] = [
+ template.querySelector('span.after-text'),
+ template.querySelector('[data-field="number"] b'),
+ template.querySelector('[data-field="started"] time'),
+ template.querySelector('[data-field="updated"] time')
+ ];
+
+ if (afterEl) {
+ afterEl.textContent = `After ${item.number - 1} Tx`;
+ }
+ if (number) {
+ number.textContent = item.number - 1;
+ }
+ 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']);
}
}
removePlaceholders() {
- if (this.grid.querySelector('.placeholder')) {
- window.removeChildren(this.grid);
+ const placeholders = this.ui.grid.querySelectorAll('.placeholder');
+ if (placeholders.length > 0) {
+ placeholders.forEach(p => p.remove());
}
}
- 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'
+ defineTemplates() {
+ const T = this.templates;
+ const f = this;
+
+ T.define('feedTerm', {
+ refs: {
+ icon: '.icon',
+ span: 'span'
},
- () => 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];
+ 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);
}
});
- }
- /**
- * Get image size based on screen width
- */
- getImageSize() {
- const width = window.innerWidth;
- if (width > 1024) return 'medium';
- if (width > 500) return 'medium';
- return 'small';
+ 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();
+ // 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', () => {
- window.feedBlock = new FeedBlock();
+
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe(event => {
+ if (event === 'auth-loaded') {
+ window.feedBlock = new FeedBlock();
+ }
+ });
});
--
Gitblit v1.10.0