From 0afb2c0046b55c123eafb4ab9ee77efa68d12463 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 06 Jun 2026 17:15:31 +0000
Subject: [PATCH] =Starting the Favourites.js setup, converting previous Northeh stuff to new Registrar, fixing up Square.php integration to match
---
src/feed/view.js | 364 +++++++++++++++++++++++++++++++--------------------
1 files changed, 219 insertions(+), 145 deletions(-)
diff --git a/src/feed/view.js b/src/feed/view.js
index 4e118c4..a8c9946 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -6,9 +6,11 @@
this.a11y = window.jvbA11y;
this.error = window.jvbError;
this.cache = new window.jvbCache('feed');
+ this.templates = window.jvbTemplates;
+ this.isFirstLoad = true;
this.config = {
- source: '',
+ contextId: '',
context: '',
highlight: null,
gallery: false,
@@ -16,26 +18,23 @@
... this.container.dataset
};
+
this.init();
}
init() {
this.initElements();
+ this.defineTemplates();
+
this.initListeners();
this.initFilters();
- if ('requestIdleCallback' in window) {
- requestIdleCallback(() => {
- this.initStore();
- this.initTaxonomies();
- this.initGallery();
- }, { timeout: 2000 });
- } else {
- setTimeout(() => {
- this.initStore();
- this.initTaxonomies();
- this.initGallery();
- }, 100);
- }
+ this.initStore();
+ this.initTaxonomies().then(r => {});
+
+ this.processCachedFilters();
+ this.processURLFilters();
+ this.updateFilterUI();
+ this.initGallery();
}
initElements() {
@@ -43,10 +42,13 @@
filterTrigger: '[data-filter]',
filters: {
actions: '.filter-actions .toggle-text',
- container: '.filters',
+ container: '.all-filters',
+ showing: '.all-filters summary .current',
content: '[data-filter="content"]',
+ ordering: '.ordering',
orderby: '[data-filter="orderby"]',
order: '[data-filter="order"]',
+ orderWrap: '.order-direction',
match: '[data-filter="match"]',
favourites: '[data-filter="favourites"]',
taxonomy: '[data-filter^="taxonomy"]',
@@ -56,31 +58,49 @@
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;
- this.ui.orderbyWrap = this.ui.filters.container.querySelector('[data-for-order]');
- if (this.ui.orderbyWrap.length === 0) this.ui.content = false;
- this.ui.order = this.ui.filters.container.querySelectorAll('[data-filter="order"]');
- if (this.ui.order.length === 0) this.ui.content = false;
- this.ui.orderby = this.ui.filters.container.querySelectorAll('[data-filter="orderby"]');
- if (this.ui.orderby.length === 0) this.ui.content = false;
+ let getAll = ['content','orderby','order','taxonomy'];
+ getAll.forEach(item => {
+ let items = this.ui.filters.container.querySelectorAll(this.selectors.filters[item]);
+ this.ui[item] = Array.from(items);
+ });
- this.contentTypes = (this.ui.content)
- ? Array.from(this.ui.content).map(c => c.value)
+ this.contentTypes = (this.ui.content.length > 0)
+ ? 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)
: [];
}
+ /**
+ *
+ * @param {string} item
+ */
+ getChecked(item) {
+ if (!['content', 'orderby','order'].includes(item)) {
+ console.log('Invalid item to check: ', item);
+ }
+
+ let items = this.ui[item];
+ if (!items) {
+ return;
+ }
+
+ let checked = items.filter(i => i.checked);
+ if (item === 'content' && checked.length > 0) {
+ this.updateContentFor(checked[0].value);
+ }
+ return checked.length === 0 ? items[0].value : checked[0].value;
+ }
+
initListeners() {
this.popStateHandler = this.handlePopState.bind(this);
this.clickHandler = this.handleClick.bind(this);
@@ -94,18 +114,16 @@
initFilters() {
this.allowedFilters = ['content', 'order', 'orderby', 'favourites', 'match'];
let defaults = {
- content: this.contentTypes[0],
- orderby: 'date',
- order: 'desc',
+ content: this.getChecked('content'),
+ orderby: this.getChecked('orderby'),
+ order: this.getChecked('order'),
page: 1,
};
if (this.config.context) defaults.context = this.config.context;
- if (this.config.source) defaults.source = this.config.source;
+ if (this.config.contextId) defaults.contextId = this.config.contextId;
this.filters = defaults;
- this.processCachedFilters();
- this.processURLFilters();
- this.updateFilterUI();
+
this.defaults = {...defaults};
}
updateFilterUI() {
@@ -121,8 +139,8 @@
if(group) {
for (let input of group) {
let [filter, value] = [input.dataset.filter, input.value];
- if (!Object.hasOwn(this.filters, filter)) break;
- let doit = this.filters[filter] === value;
+ if (!Object.hasOwn(this.store.filters, filter)) break;
+ let doit = this.store.filters[filter] === value;
if (doit) {
input.checked = doit;
break;
@@ -131,13 +149,13 @@
}
});
- if (Object.hasOwn(this.filters, 'taxonomy')) {
- for (let [taxonomy, terms] of Object.entries(this.filters.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(term);
+ this.createTermElement(termId);
}
});
}
@@ -164,6 +182,18 @@
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() {
@@ -204,6 +234,9 @@
...this.defaults,
taxonomy: null
});
+
+ this.updateURL();
+ this.saveToCacheFilters();
}
resetFilters(filters) {
@@ -213,10 +246,13 @@
... 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);
+ return this.selector.getFieldId(this.ui.taxonomies.filter(tax => tax.dataset.taxonomy === taxonomy)[0]??null);
}
removeSelectedTerm(button) {
const termId = parseInt(button.dataset.id);
@@ -251,6 +287,8 @@
this.ui.taxonomies,
this.ui.orderby
];
+
+ this.ui.filters.showing.textContent = this.ui.content.filter(c => c.value === content)[0].dataset.label;
checkIt.forEach(check => {
if (!check) return;
check.forEach(button => {
@@ -263,19 +301,19 @@
});
}
updateOrderOptions(order) {
- if (this.ui.orderbyWrap) {
- let options = this.ui.orderbyWrap.dataset.forOrder.split(',')??[];
- this.ui.orderbyWrap.hidden = !options.includes(order);
+ if (this.ui.filters.orderWrap) {
+ let options = this.ui.filters.orderWrap.dataset.forOrder.split(',')??[];
+ this.ui.filters.orderWrap.hidden = !options.includes(order);
}
}
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;
}
}
@@ -307,7 +345,7 @@
this.updateFilterControls();
}
getTaxonomyIcon(taxonomy) {
- let iconButton = Array.from(this.ui.taxonomies)
+ let iconButton = this.ui.taxonomies
.find(t => t.dataset.taxonomy === taxonomy);
return iconButton?.dataset.icon.trim() || 'tag';
}
@@ -315,22 +353,14 @@
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() {
Object.keys(this.filters).forEach(filter => {
- let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`);
+ let cached = this.cache.get(`${this.config.contextId}_${this.config.context}_${filter}`);
if (cached && cached !== this.filters[filter]) {
this.filters[filter] = cached;
}
@@ -338,31 +368,41 @@
}
processURLFilters() {
- if (this.filters.page > 1) return false;
+ 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.filters, key) && this.filters[key] !== this.defaults[key]) {
- params.set(`f_${key}`, this.filters[key]);
+ if (Object.hasOwn(this.store.filters, key) && this.store.filters[key] !== this.defaults[key]) {
+ params.set(`f_${key}`, this.store.filters[key]);
}
});
@@ -371,29 +411,31 @@
params.set(`f_tax_${tax}`, terms.join(','));
}
}
+
const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
- if (newURL !== window.location.pathname) {
- window.history.pushState({filters:this.filters}, '', newURL)
+ const currentURL = window.location.pathname + window.location.search; // Change this line
+
+ if (newURL !== currentURL) {
+ window.history.pushState({filters:this.store.filters}, '', newURL);
}
}
saveToCacheFilters() {
- // Save each filter that's changed from defaults
- Object.keys(this.filters).forEach(filter => {
- if (this.filters[filter] !== this.defaults[filter]) {
- this.cache.set(
- `${this.config.source}_${this.config.context}_${filter}`,
- this.filters[filter]
- );
+ Object.keys(this.store.filters).forEach(filter => {
+ const cacheKey = `${this.config.contextId}_${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);
}
});
- // Also save taxonomy filters
+ const taxCacheKey = `${this.config.contextId}_${this.config.context}_taxonomy`;
if (Object.keys(this.taxFilters).length > 0) {
- this.cache.set(
- `${this.config.source}_${this.config.context}_taxonomy`,
- this.taxFilters
- );
+ this.cache.set(taxCacheKey, this.taxFilters);
+ } else {
+ this.cache.remove(taxCacheKey);
}
}
@@ -408,6 +450,12 @@
}
}
initStore() {
+ let extraOrderby = this.ui.orderby.filter(v => !['date','date_modified','title','random'].includes(v.value));
+
+ let extraIndexes = [];
+ extraOrderby.forEach(orderby =>{
+ extraIndexes.push({name:orderby.value, keyPath: orderby.value});
+ });
const store = window.jvbStore.register(
'feed',
{
@@ -418,30 +466,38 @@
{ 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
showLoading: true,
required: 'content',
- }
+ },
+ 2
);
+
this.store = store.feed;
this.store.subscribe((event, data) => {
switch (event) {
case 'data-loaded':
- this.renderItems();
+ if (this.isFirstLoad) {
+ //We rendered the first page in php already
+ this.isFirstLoad = false;
+ return;
+ }
+ // if (this.store.filters.page === 1) {
+ // return;
+ // }
+ 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;
- case 'filters-changed':
- this.updateURL();
- this.saveToCacheFilters();
- break;
}
});
}
@@ -450,8 +506,8 @@
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);
}
@@ -459,26 +515,18 @@
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();
@@ -486,48 +534,15 @@
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);
@@ -598,14 +613,16 @@
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);
}
@@ -623,7 +640,7 @@
element.textContent = window.formatTimeAgo(value, 'F Y');
}
formatField(element, value) {
- element.textContent = value;
+ element.textContent = window.decodeHTMLEntities(value);
}
addTimelineElements(item, template) {
@@ -640,10 +657,10 @@
];
if (afterEl) {
- afterEl.textContent = `After ${item.fields.number} Tx`;
+ afterEl.textContent = `After ${item.number} Tx`;
}
if (number) {
- number.textContent = item.fields.number;
+ number.textContent = item.number;
}
if (started) {
this.formatTimeField(started, item.fields.timeline[0]['post_date']);
@@ -660,6 +677,63 @@
}
}
+ 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();
--
Gitblit v1.10.0