class FeedBlock { constructor() { this.container = document.querySelector('section.feed-block'); if(!this.container) return; this.a11y = window.jvbA11y; this.error = window.jvbError; this.cache = new window.jvbCache('feed'); this.templates = window.jvbTemplates; this.config = { source: '', 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(); 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.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() { 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); } 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; } } } }); 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(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(); // 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 ]; 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() { 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 = 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; } }); } 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.source}_${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.source}_${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(); } }); } } 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', } ); 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; } }); } 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() { const placeholders = this.ui.grid.querySelectorAll('.placeholder'); if (placeholders.length > 0) { placeholders.forEach(p => p.remove()); } } defineTemplates() { const T = this.templates; const f = this; T.define('feedTerm', { refs: { icon: '.icon', span: 'span' }, setup({el, refs, manyRefs, data}) { el.dataset.id = data.id; el.dataset.taxonomy = data.taxonomy; if (refs.icon) refs.icon.className = `icon icon-${data.icon}`; if (refs.span) refs.span.textContent = window.decodeHTMLEntities(data.name); } }); T.define('emptyState'); this.contentTypes.forEach(content => { T.define(`feedItem${window.uppercaseFirst(content)}`, { refs: { link: 'a', }, manyRefs: { fields: '[data-field]', }, setup({el, refs, manyRefs, data}) { const isTimeline = Object.hasOwn(el.dataset, 'timeline'); if (manyRefs.fields) { for (let field of manyRefs.fields) { if (isTimeline && ['timeline','number'].includes(field.dataset.field)) continue; const value = Object.hasOwn(data.fields, field.dataset.field)? data.fields[field.dataset.field] : false; if (!value) { field.remove(); continue; } if (f.isImageField(data, value)) { f.formatImageField(field, value, data); } else if (f.isTaxonomyField(data, field.dataset.field)) { f.formatTaxonomyField(field, data, field.dataset.field, value); } else if (f.isTimeField(field)) { f.formatTimeField(field, value); } else { f.formatField(field, value); } } if (refs.link && data.url !== '') { refs.link.href = data.url; refs.link.title = `View ${data.fields['post_title']??'Item'}`; } if (isTimeline ) f.addTimelineElements(data, el); } } }) }); } // addPlaceholders() { // let total = this.contentTypes.length; // const fragment = document.createDocumentFragment(); // 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() { window.auth.subscribe(event => { if (event === 'auth-loaded') { window.feedBlock = new FeedBlock(); } }); });