| | |
| | | this.a11y = window.jvbA11y; |
| | | this.error = window.jvbError; |
| | | this.cache = new window.jvbCache('feed'); |
| | | this.templates = window.jvbTemplates; |
| | | |
| | | this.config = { |
| | | source: '', |
| | |
| | | } |
| | | init() { |
| | | this.initElements(); |
| | | this.defineTemplates(); |
| | | this.initListeners(); |
| | | this.initFilters(); |
| | | |
| | |
| | | filterTrigger: '[data-filter]', |
| | | filters: { |
| | | actions: '.filter-actions .toggle-text', |
| | | container: '.filters', |
| | | container: '.all-filters', |
| | | content: '[data-filter="content"]', |
| | | orderby: '[data-filter="orderby"]', |
| | | order: '[data-filter="order"]', |
| | |
| | | buttons: { |
| | | loadMore: 'button.load-more', |
| | | remove: '.remove-term', |
| | | clearFilters: 'button.clear-filters' |
| | | 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.content = false; |
| | | 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.content = false; |
| | | 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.content = false; |
| | | 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.content = false; |
| | | 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) |
| | |
| | | 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() { |
| | |
| | | } |
| | | |
| | | updateFilterControls() { |
| | | const isHidden = Object.keys(this.taxFilters).length === 0; |
| | | const keys = Object.keys(this.taxFilters); |
| | | if (this.ui.buttons.clearFilters) { |
| | | this.ui.buttons.clearFilters.hidden = isHidden; |
| | | this.ui.buttons.clearFilters.hidden = keys.length === 0; |
| | | } |
| | | if (this.ui.filters.actions) { |
| | | this.ui.filters.actions.hidden = isHidden; |
| | | this.ui.filters.actions.hidden = keys.length <= 1; |
| | | } |
| | | } |
| | | |
| | |
| | | const term = this.selector.store.get(termId); |
| | | if (!term) return; |
| | | if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return; |
| | | let icon = this.getTaxonomyIcon(term.taxonomy); |
| | | let template = window.getTemplate('feedTerm'); |
| | | if (!template) return; |
| | | let [iconEl,span] = [template.querySelector('.icon'), template.querySelector('span')]; |
| | | if (!iconEl || !span) return; |
| | | template.dataset.id = term.id; |
| | | template.dataset.taxonomy = term.taxonomy; |
| | | iconEl.className = `icon icon-${icon}`; |
| | | span.textContent = term.name; |
| | | |
| | | this.ui.selected.append(template); |
| | | term.icon = this.getTaxonomyIcon(term.taxonomy); |
| | | this.ui.selected.append(this.templates.create('feedTerm', term)); |
| | | } |
| | | |
| | | processCachedFilters() { |
| | |
| | | } |
| | | } |
| | | 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', |
| | | { |
| | |
| | | { 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, //6 hours |
| | |
| | | required: 'content', |
| | | } |
| | | ); |
| | | |
| | | this.store = store.feed; |
| | | |
| | | this.store.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'data-loaded': |
| | | this.renderItems(); |
| | | 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; |
| | |
| | | return this.store.filters.page === 1; |
| | | } |
| | | |
| | | renderItems() { |
| | | let items = this.store.getFiltered(); |
| | | renderItems(items = null) { |
| | | items = items??this.store.getFiltered(); |
| | | if (this.isFirstPage()) { |
| | | window.removeChildren(this.ui.grid); |
| | | } |
| | |
| | | this.showEmptyState(); |
| | | this.a11y.announceItems(0, this.isFirstPage()); |
| | | } else { |
| | | const fragment = document.createDocumentFragment(); |
| | | const processBatch = (startIndex) => { |
| | | const endIndex = Math.min(startIndex + 10, items.length); |
| | | for (let i = startIndex; i < endIndex; i++) { |
| | | const item = items[i]; |
| | | const element = this.createItemElement(item); |
| | | fragment.append(element); |
| | | } |
| | | if (endIndex < items.length) { |
| | | requestAnimationFrame(() => processBatch(endIndex)); |
| | | } 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); |
| | | } |
| | | }; |
| | | processBatch(0); |
| | | }, |
| | | 5 |
| | | ).then(()=>{}); |
| | | } |
| | | |
| | | this.updateFilterControls(); |
| | |
| | | |
| | | showEmptyState() { |
| | | window.removeChildren(this.ui.grid); |
| | | let template = window.getTemplate('emptyState'); |
| | | if (!template) return; |
| | | this.ui.grid.append(template); |
| | | |
| | | this.ui.grid.append(this.templates.create('emptyState')); |
| | | } |
| | | |
| | | createItemElement(item) { |
| | | let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`); |
| | | const isTimeline = Object.hasOwn(template.dataset, 'timeline'); |
| | | |
| | | for (let [fieldName, value] of Object.entries(item.fields)) { |
| | | if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue; |
| | | |
| | | let el = template.querySelector(`[data-field="${fieldName}"]`); |
| | | if (!el) continue; |
| | | if (value === '') { |
| | | el.remove(); |
| | | continue; |
| | | } |
| | | |
| | | if (this.isImageField(item, value)) { |
| | | this.formatImageField(el, value, item); |
| | | } else if (this.isTaxonomyField(item, fieldName)) { |
| | | this.formatTaxonomyField(el, item, fieldName, value); |
| | | } else if (this.isTimeField(el)) { |
| | | this.formatTimeField(el, value); |
| | | } else { |
| | | this.formatField(el, value); |
| | | } |
| | | if (typeof item !== 'object') { |
| | | item = this.store.get(item); |
| | | if (!item) return; |
| | | } |
| | | |
| | | let link = template.querySelector('a'); |
| | | if (link && item.url !== '') { |
| | | [link.href, link.title] = |
| | | [item.url, `View ${item.fields['post_title']??'Item'}`]; |
| | | } |
| | | |
| | | if (isTimeline) { |
| | | this.addTimelineElements(item, template); |
| | | } |
| | | |
| | | return template; |
| | | return this.templates.create(`feedItem${window.uppercaseFirst(item.content)}`, item); |
| | | } |
| | | splitIDs(value) { |
| | | return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value); |
| | |
| | | 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); |
| | | } |
| | |
| | | element.textContent = window.formatTimeAgo(value, 'F Y'); |
| | | } |
| | | formatField(element, value) { |
| | | element.textContent = value; |
| | | element.textContent = window.decodeHTMLEntities(value); |
| | | } |
| | | |
| | | addTimelineElements(item, template) { |
| | |
| | | ]; |
| | | |
| | | if (afterEl) { |
| | | afterEl.textContent = `After ${item.fields.number} Tx`; |
| | | afterEl.textContent = `After ${item.number - 1} Tx`; |
| | | } |
| | | if (number) { |
| | | number.textContent = item.fields.number; |
| | | number.textContent = item.number - 1; |
| | | } |
| | | if (started) { |
| | | this.formatTimeField(started, item.fields.timeline[0]['post_date']); |
| | |
| | | } |
| | | } |
| | | |
| | | 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(); |