From 47e77f9fac1155c536b2b87fec552c7fcce66fa6 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 01 Jun 2026 18:06:34 +0000
Subject: [PATCH] =Timeline block fixes. Next up: adding article schema classes
---
src/feed/viewOld.js | 770 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 770 insertions(+), 0 deletions(-)
diff --git a/src/feed/viewOld.js b/src/feed/viewOld.js
new file mode 100644
index 0000000..c65be60
--- /dev/null
+++ b/src/feed/viewOld.js
@@ -0,0 +1,770 @@
+class FeedBlockOld {
+ constructor() {
+ this.container = document.querySelector('section.feed-block');
+ if (!this.container) {
+ return;
+ }
+
+ this.a11y = window.jvbA11y;
+ this.cache = new window.jvbCache('feed');
+ this.error = window.jvbError;
+
+ this.config = {
+ source: '',
+ context: '',
+ highlight: null,
+ gallery: false,
+ view: this.cache.get('feedView') || 'grid',
+ ... this.container.dataset
+ };
+ this.initElements();
+ this.initFilters();
+
+
+ this.loadWhenAble();
+ }
+
+ loadWhenAble() {
+ if ('requestIdleCallback' in window) {
+ requestIdleCallback(() => {
+ this.initTaxonomies();
+ this.initStore();
+ this.initListeners();
+ this.initGallery();
+ }, { timeout: 2000 });
+ } else {
+ setTimeout(() => {
+ this.initTaxonomies();
+ this.initStore();
+ this.initListeners();
+ this.initGallery();
+ }, 100);
+ }
+ }
+
+ initElements() {
+ this.currentTaxonomies = new Set(); // Allowed Taxonomies, grabbed from active buttons
+ this.taxonomyFilters = {};
+ this.elements = {
+ filterTrigger: '[data-filter]',
+ 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"]'
+ },
+ selectedTax: '.selected-items',
+ clearFilter: 'button.clear-filters',
+ loadMore: 'button.load-more',
+ filterContainer: '.filters',
+ grid: '.item-grid',
+ };
+ this.ui = window.uiFromSelectors(this.elements);
+
+
+ this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
+ this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
+ if (this.ui.content && this.ui.content.length > 0) {
+ this.contentTypes = Array.from(
+ this.ui.content
+ ).map(content => content.value);
+ } else {
+ this.contentTypes = [this.container.dataset['content']];
+ }
+
+ if (this.ui.taxonomies.length>0) {
+ this.taxonomies = Array.from(
+ this.ui.taxonomies,
+ ).map(content => content.dataset.taxonomy);
+ } else {
+ this.taxonomies = [];
+ }
+
+
+ }
+
+ async initTaxonomies() {
+ this.selector = window.jvbSelector;
+ const buttons = document.querySelectorAll('[data-filter="taxonomy"]');
+
+ this.selector.isInitializing = true;
+ buttons.forEach((button) => {
+ const taxonomy = button.dataset.taxonomy;
+ this.currentTaxonomies.add(taxonomy);
+
+ this.selector.registerFilterButton(button, {
+ button: button,
+ buttonSelector: '[data-filter="taxonomy"]',
+ selected: this.ui.selectedTax
+ });
+
+ // Add preload listeners
+ this.addTaxonomyPreloadListeners(button, taxonomy);
+ });
+
+ this.selector.isInitializing = false;
+
+ this.selector.subscribe((event, data) => {
+ if (event === 'selected-terms') this.handleTaxonomyChange(data);
+ });
+ }
+
+ addTaxonomyPreloadListeners(button, taxonomy) {
+ const preload = () => {
+ this.selector.preloadTaxonomy(taxonomy);
+ };
+
+ // Desktop hover
+ button.addEventListener('mouseenter', preload, { once: true });
+
+ // Touch/keyboard (fires before click)
+ button.addEventListener('pointerdown', preload, { once: true });
+
+ // Keyboard focus
+ button.addEventListener('focus', preload, { once: true });
+ }
+
+ handleTaxonomyChange(data) {
+ const { terms, taxonomy } = data;
+
+ // Update only the current taxonomy's terms
+ if (terms.size > 0) {
+ this.taxonomyFilters[taxonomy] = Array.from(terms.keys());
+ } else {
+ // Remove taxonomy if no terms selected
+ delete this.taxonomyFilters[taxonomy];
+ }
+
+ // Build filters object with all taxonomies
+ let filters = {
+ page: 1
+ };
+
+ // Add taxonomy filters if any exist
+ if (Object.keys(this.taxonomyFilters).length > 0) {
+ filters.taxonomy = this.taxonomyFilters;
+ }
+
+ this.updateFilter(filters);
+ }
+
+ clearAllTaxonomies() {
+ this.taxonomyFilters = {};
+ window.removeChildren(this.ui.selectedTax);
+
+ this.updateFilter({
+ taxonomy: null,
+ page: 1
+ });
+ }
+
+ initFilters() {
+ //defaults
+ this.filters = {
+ content: this.contentTypes[0],
+ orderby: 'date',
+ order: 'desc',
+ page: 1
+ };
+ if (this.config.context) this.filters.context = this.config.context;
+ if (this.config.source) this.filters.source = this.config.source;
+
+ //check the cache
+ this.processCachedFilters();
+ //check url
+ this.processURLFilters();
+
+ // Set initial UI state
+ this.syncUIToFilters();
+ }
+ syncUIToFilters() {
+ if (this.ui.filterContainer) {
+ // Check radio buttons
+ Object.entries(this.filters).forEach(([key, value]) => {
+ const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
+ if (input) {
+ input.checked = true;
+ }
+ });
+ }
+
+ // Update content-specific visibility
+ this.updateContentFor(this.filters.content);
+ }
+ nextPage() {
+ this.store.setFilter('page', this.store.filters.page++);
+ }
+
+ initStore() {
+ 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: 'modified'},
+ { name: 'title', keyPath: 'title'}
+ ],
+ filters: this.filters,
+ TTL: 6 * 60 * 60 * 1000,
+ showLoading: true,
+ required: 'content',
+ delayFetch: true
+ }
+ );
+ this.store = store.feed;
+
+ this.store.subscribe((event, data) => {
+ switch (event) {
+ case 'data-loaded':
+ this.renderItems();
+ this.ui.loadMore.hidden = true;
+ if (this.store.lastResponse && this.store.lastResponse['has_more']) {
+ this.ui.loadMore.hidden = !this.store.lastResponse['has_more'];
+ }
+ break;
+ }
+ });
+ }
+
+ initGallery() {
+ this.gallery = (this.config.gallery) ? window.jvbGallery : false;
+ if (this.gallery) {
+ this.gallery.subscribe((event, data) => {
+ if (event === 'load-more' && this.store.lastResponse) {
+ if (this.store.lastResponse['has_more']) {
+ this.nextPage();
+ }
+ }
+ });
+ }
+ }
+
+ 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.filters.page > 1) {
+ return false;
+ }
+ const params = new URLSearchParams(window.location.search);
+
+ if (!params.toString()) {
+ return false;
+ }
+ let filters = ['content', 'order', 'orderby', 'favourites', 'match'];
+ filters.forEach(filter => {
+ let value = params.get(`f_${filter}`);
+ if (value) {
+ this.filters[filter] = value;
+ let input = this.ui.filters[filter];
+ if (input) {
+ input.checked = true;
+ }
+ }
+ });
+
+ let hasTaxonomy = false;
+ // Load taxonomy filters from URL
+ params.forEach((value, key) => {
+ if (key.startsWith('f_tax_')) {
+ hasTaxonomy = true;
+ const taxonomy = key.replace('f_tax_', '');
+ if (!this.taxonomyFilters[taxonomy]) {
+ this.taxonomyFilters[taxonomy] = [];
+ }
+ this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
+ }
+ });
+ if (this.ui.filterContainer && hasTaxonomy) {
+ for (let [tax, ids] in Object.entries(this.taxonomyFilters)) {
+ let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`);
+ if (button) {
+ if (button.dataset.fieldId) {
+ let field = this.selector.get(button.dataset.fieldId);
+ field.selectedTerms = new Set(ids);
+ this.selector.initFieldDisplay(button.dataset.fieldId);
+ } else {
+ this.selector.registerField(button, {
+ button: button,
+ buttonSelector: '[data-filter="taxonomy"]',
+ selected: this.ui.selectedTax,
+ selectedItems: ids
+ });
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Update URL with current filters (for sharing/bookmarking)
+ */
+ updateURL() {
+ const params = new URLSearchParams();
+
+ // Add simple filters
+ ['content', 'order', 'orderby', 'match'].forEach(key => {
+ if (this.filters[key]) {
+ params.set(`f_${key}`, this.filters[key]);
+ }
+ });
+
+ // Add taxonomy filters
+ Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => {
+ if (terms.length > 0) {
+ params.set(`f_tax_${taxonomy}`, terms.join(','));
+ }
+ });
+
+ // Update URL without reload
+ const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
+ window.history.pushState({ filters: this.filters }, '', newURL);
+ }
+
+ renderItems() {
+ let items = this.store.getFiltered();
+ if (this.store.filters['page'] === 1) {
+ window.removeChildren(this.ui.grid);
+ }
+
+ if (items.length === 0) {
+ this.a11y.announceItems(0, this.store.filters['page'] > 0);
+ return;
+ }
+
+ 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);
+ }
+
+ if (endIndex < items.length) {
+ requestAnimationFrame(() => processBatch(endIndex));
+ } else {
+ this.removePlaceholders();
+ this.ui.grid.append(fragment);
+
+ if (this.config.gallery) {
+ this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
+ }
+
+ this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+ this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore);
+ }
+ };
+
+ if (items.length > 0) {
+ processBatch(0);
+ } else {
+ this.a11y.announceItems(0, this.store.filters['page'] >1, false);
+ }
+
+ if (this.ui.filters.match) {
+ this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
+ }
+ if (this.ui.clearFilter) {
+ this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
+ }
+ }
+
+ /**
+ *
+ * @param {object} item
+ */
+ createItemElement(item) {
+ let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
+
+ const isTimeline = Object.hasOwn(template.dataset, 'timeline');
+
+ // Format fields using helpers
+ 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.formatImageFields(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);
+ }
+ }
+
+ // Handle link
+ 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;
+ }
+ 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;
+
+ [
+ link.href,
+ link.title,
+ link.textContent
+ ] = [
+ term.url,
+ `See more ${term.title}`,
+ term.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 = 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.fields.number} Tx`;
+ }
+ if (number) {
+ number.textContent = item.fields.number;
+ }
+ 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());
+ }
+ }
+
+
+ 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);
+ }
+
+
+
+ /**
+ *
+ * @param {object} filters {name: value}
+ */
+ updateFilter(filters) {
+ //double check filters are what we're expecting
+ let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)];
+
+ filters = Object.keys(filters)
+ .filter(key => allowed.includes(key))
+ .reduce((obj, key) => {
+ obj[key] = filters[key];
+ return obj;
+ }, {});
+
+ if (window.getDifferences.map(this.filters, filters)) {
+ this.filters = { ...this.filters, ...filters }; // Merge instead of replace
+ this.updateURL();
+ this.store.setFilters(filters);
+ }
+ }
+ /**
+ * Update visible filters based on selected content type
+ */
+ updateContentFor(contentType) {
+ // Update taxonomy filter visibility
+ const taxonomyButtons = this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]');
+ taxonomyButtons.forEach(button => {
+ const forTypes = button.dataset.for?.split(',') || [];
+ button.hidden = forTypes.length > 0 && !forTypes.includes(contentType);
+ });
+
+ // Update ordering options
+ const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]');
+ orderButtons.forEach(button => {
+ const forTypes = button.dataset.for?.split(',') || [];
+ if (forTypes.length > 0) {
+ button.hidden = !forTypes.includes(contentType);
+ // Uncheck if hiding
+ if (button.hidden && button.checked) {
+ button.checked = false;
+ }
+ }
+ });
+
+ // Update order direction visibility based on selected orderby
+ const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked');
+ this.updateOrderDirectionVisibility(orderBy?.value);
+ }
+
+ /**
+ * Show/hide order direction based on orderby selection
+ */
+ updateOrderDirectionVisibility(orderBy) {
+ const orderDirection = this.ui.filterContainer.querySelector('.order-direction');
+ if (orderDirection) {
+ const forOrders = orderDirection.dataset.forOrder?.split(',') || [];
+ orderDirection.hidden = forOrders.length > 0 && !forOrders.includes(orderBy);
+ }
+ }
+ /*********************************************************************
+ LISTENERS
+ *********************************************************************/
+ initListeners() {
+ this.popStateHandler = this.handlePopState.bind(this);
+ this.clickHandler = this.handleClick.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
+ this.imageObserver = null;
+ this.resizeObserver = null;
+ if ('IntersectionObserver' in window) {
+ this.imageObserver = new IntersectionObserver(entries => {
+ entries.forEach(entry => {
+ this.loadImage(entry.target);
+ this.imageObserver.unobserve(entry.target);
+ });
+ }, {
+ rootMargin: '100px',
+ threshold: .1
+ });
+ }
+
+ if ('ResizeObserver' in window) {
+ this.resizeObserver = new ResizeObserver(() => {
+ window.debouncer.schedule(
+ 'feed-update-images',
+ () => this.updateImageSizes(),
+ 250
+ );
+ });
+ } else {
+ window.addEventListener('resize', () => {
+ window.debouncer.schedule(
+ 'feed-update-images',
+ () => this.updateImageSizes(),
+ 250
+ );
+ });
+ }
+
+ window.addEventListener('popstate', this.popStateHandler);
+ document.addEventListener('click', this.clickHandler);
+ document.addEventListener('change', this.changeHandler);
+ }
+
+ 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.elements.loadMore)) {
+ this.nextPage();
+ } else if (window.targetCheck(e, this.elements.clearFilter)) {
+ this.clearAllTaxonomies();
+ } else if (window.targetCheck(e, '.remove-item')) {
+ this.handleRemoveSelectedTerm(e);
+ }
+ }
+
+ handleRemoveSelectedTerm(e) {
+ const selectedItem = e.target.closest('.selected-item');
+ if (!selectedItem) return;
+
+ const termId = parseInt(selectedItem.dataset.id);
+ const taxonomy = selectedItem.dataset.taxonomy;
+
+ // Remove from filters
+ if (this.taxonomyFilters[taxonomy]) {
+ this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy]
+ .filter(id => id !== termId);
+
+ if (this.taxonomyFilters[taxonomy].length === 0) {
+ delete this.taxonomyFilters[taxonomy];
+ }
+ }
+
+ // Remove from UI
+ selectedItem.remove();
+
+ // Update filters
+ this.updateFilter({
+ taxonomy: Object.keys(this.taxonomyFilters).length > 0
+ ? this.taxonomyFilters
+ : null,
+ page: 1
+ });
+ }
+
+ handleChange(e) {
+ let target = e.target;
+ if (Object.hasOwn(target.dataset, 'filter')) {
+ if (target.dataset.filter === 'content') {
+ this.updateContentFor(target.value);
+ this.updateFilter({ content: target.value, page: 1 });
+ } else if (target.dataset.filter === 'orderby') {
+ this.updateOrderDirectionVisibility(target.value);
+ this.updateFilter({ orderby: target.value, page: 1 });
+ } else if (target.dataset.filter === 'order') {
+ this.updateFilter({ order: target.value, page: 1 });
+ } else if (target.dataset.filter === 'match') {
+ this.updateFilter({ match: target.checked ? 'all' : 'any', page: 1 });
+ } else if (target.dataset.filter === 'favourites') {
+ this.updateFilter({ favourites: target.checked, page: 1 });
+ }
+ }
+ }
+}
+
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe(event => {
+ if (event === 'auth-loaded') {
+ window.feedBlock = new FeedBlock();
+ }
+ });
+});
--
Gitblit v1.10.0