Jake Vanderwerf
2026-01-01 8d0e2130627497b55b1a61cbe374bfb309ef2f27
src/feed/view.js
@@ -55,7 +55,7 @@
            favourites: '[data-filter="favourites"]',
            taxonomy: '[data-filter^="taxonomy"]'
         },
         selectedTax: '.selected-terms',
         selectedTax: '.selected-items',
         clearFilter: 'button.clear-filters',
         loadMore: 'button.load-more',
         filterContainer: '.filters',
@@ -63,15 +63,17 @@
      };
      this.ui = window.uiFromSelectors(this.elements);
      this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]');
      this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
      this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
      if (this.ui.content.length > 0) {
      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,
@@ -79,6 +81,8 @@
      } else {
         this.taxonomies = [];
      }
   }
   async initTaxonomies() {
@@ -89,24 +93,39 @@
      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);
      });
      // Make this non-blocking
      setTimeout(() => {
         this.selector.batchFetchTaxonomies();
         this.selector.isInitializing = false;
      }, 0);
      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;
@@ -161,13 +180,15 @@
      this.syncUIToFilters();
   }
   syncUIToFilters() {
      // 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;
         }
      });
      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);
@@ -177,10 +198,10 @@
   }
   initStore() {
      this.addPlaceholders();
      this.store = window.jvbStore.register(
      const store = window.jvbStore.register(
         'feed',
         {
            storeName: 'feed',
            endpoint: 'feed',
            keyPath: 'id',
            indexes: [
@@ -197,6 +218,7 @@
            delayFetch: true
         }
      );
      this.store = store.feed;
      this.store.subscribe((event, data) => {
         switch (event) {
@@ -266,7 +288,7 @@
            this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
         }
      });
      if (hasTaxonomy) {
      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) {
@@ -317,7 +339,6 @@
      let items = this.store.getFiltered();
      if (this.store.filters['page'] === 1) {
         window.removeChildren(this.ui.grid);
         this.addPlaceholders();
      }
      if (items.length === 0) {
@@ -344,9 +365,6 @@
            this.removePlaceholders();
            this.ui.grid.append(fragment);
            // Observe images after adding to DOM
            this.observeImages(this.ui.grid);
            if (this.config.gallery) {
               this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
            }
@@ -362,8 +380,12 @@
         this.a11y.announceItems(0, this.store.filters['page'] >1, false);
      }
      this.ui.filters.match.hidden = window.isEmptyObject(this.taxonomyFilters);
      this.ui.clearFilter.hidden = window.isEmptyObject(this.taxonomyFilters);
      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;
      }
   }
   /**
@@ -371,92 +393,230 @@
    * @param {object} item
    */
   createItemElement(item) {
      let template = window.getTemplate('feed-item');
      let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
      if (Object.hasOwn(template.dataset, 'timeline')) {
         return this.createTimelineElement(item, template);
      }
      //fields
      for (let [fieldName, value] of Object.entries(item.fields)) {
         let el = template.querySelector(`[data-field="${fieldName}"]`);
         if (!el) {
            continue;
         }
         if (Object.keys(item.images).map((img)=> parseInt(img)).includes(value)) {
            [
               el.src,
               el.srcset,
               el.alt
            ] = [
               item.images[value].tiny,
               `${item.images[value].tiny} 50w, ${item.images[value].small} 300w, ${item.images[value].medium} 1024w`,
               item.images[value]['image-alt-text']
            ];
         } else if (el.tagName === 'TIME') {
            el.setAttribute('datetime', value);
            el.textContent = window.formatTimeAgo(value);
         } else {
            el.textContent = value;
         }
         if (value === '') {
            el.remove();
         }
      }
      let link = template.querySelector('a');
      if (link && item.url !== '') {
         [
            link.href,
            link.title
         ] = [
            item.url,
            `View ${item.fields['post_title']??'Item'}`
         ];
      }
      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) {
         console.log('Item has no images, or the images object is empty');
         return false;
      }
      let values = this.splitIDs(value);
      values.forEach(v => {
         console.log('Checking id: ', v);
         if (Object.keys(item.images).includes(parseInt(v))) {
            console.log('Item.images does not have id');
            return true;
         }
      });
      return false;
   }
   formatImageFields(element, value, item) {
      console.log('Formatting image Field: ', element);
      console.log('value: ', value);
      console.log('item: ', item);
      if (value.length === 0) return;
      //If it's a gallery, we're cloning the original image, then removing it
      if (value.length > 1) {
         let image = element.querySelector('img');
         if (!image) return;
         value.forEach(imgID => {
            let img = image.cloneNode(true);
            this.formatImageField(img, imgID, item);
            element.append(img);
         });
         image.remove();
      } else {
         console.log(element.tagName);
         if (element.tagName !== 'IMG') {
            element = element.querySelector('img');
            if (!element) return;
         }
         this.formatImageField(element, value[0], item);
      }
   }
      formatImageField(element, value, item) {
      console.log('Formatting Image:', element);
      console.log('Value: ', value);
      console.log('ImagData:', item.images[value]);
      let imgData = item.images[value]??false;
      if (!imgData) return;
         [
            element.src,
            element.srset,
            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;
      }
      Object.keys(item.taxonomies).forEach(taxonomy => {
         if (taxonomy === field) {
            return true;
         }
      });
      return false;
   }
   formatTaxonomyField(element, item, field, value) {
      if (element.tagName !== 'UL' || !element.querySelector('li')) return;
      let values = this.splitIDs(value);
      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);
   }
   formatField(element, value) {
      element.textContent = value;
   }
   createTimelineElement(item, template) {
      console.log(item);
      console.log(template);
      for (let [field, value] of Object.entries(item.fields)) {
         if (!['timeline', 'number'].includes(field)) {
            let el = template.querySelector(`[data-field="${field}"]`);
            if (!el) {
               console.log(`Element Not found for ${field}`);
            }
            if (!el || value === '') continue;
            if (this.isImageField(item, value)) {
               this.formatImageFields(el, value, item);
            } else if (this.isTaxonomyField(item, field)) {
               this.formatTaxonomyField(el, item, field, value);
            } else if (this.isTimeField(el)) {
               this.formatTimeField(el, value);
            } else {
               this.formatField(el, value);
            }
         }
      }
      let [
         main,
         link,
         beforeImg,
         afterImg,
         afterText,
         afterEl,
         number,
         started,
         lastTreated,
         total,
         termList,
         timeline
         last
      ] = [
         template,
         template.querySelector('a'),
         template.querySelector('img.before'),
         template.querySelector('img.after'),
         template.querySelector('summary span:last-of-type'),
         template.querySelector('p.started time'),
         template.querySelector('p.updated time'),
         template.querySelector('p.total b'),
         template.querySelector('.term-list'),
         Object.values(item.fields.order)
         template.querySelector('span.after-text'),
         template.querySelector('[data-field="number"] b'),
         template.querySelector('[data-field="started"] time'),
         template.querySelector('[data-field="updated"] time')
      ];
      let numberTreatments = timeline.length - 1;
      let beforeImgData = item.images[timeline[0]['post_thumbnail']];
      let afterImgData = item.images[timeline[numberTreatments]['post_thumbnail']];
      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']);
      }
      [
         main.dataset.id,
         link.href,
         beforeImg.src,
         beforeImg.dataset.small,
         beforeImg.dataset.medium,
         afterImg.src,
         afterImg.dataset.small,
         afterImg.dataset.medium,
         afterText.textContent,
         started.textContent,
         lastTreated.textContent,
         total.textContent
      ] = [
         item.id,
         item.url,
         beforeImgData['tiny'],
         beforeImgData.small,
         beforeImgData.medium,
         afterImgData['tiny'],
         afterImgData.small,
         afterImgData.medium,
         `${afterText.textContent} ${numberTreatments} Tx`,
         timeline[0].date??item.date,
         timeline[numberTreatments].date??'',
         `${numberTreatments} Treatments`
      ];
      return template;
   }
   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.length > 0) {
         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.appendChild(icon);
         this.ui.grid.appendChild(template);
         template.append(icon);
         fragment.append(template);
      }
   }
   removePlaceholders() {
      if (this.ui.grid.querySelector('.placeholder')) {
         window.removeChildren(this.ui.grid);
      }
      this.ui.grid.append(fragment);
   }
@@ -543,13 +703,21 @@
      }
      if ('ResizeObserver' in window) {
         this.resizeObserver = new ResizeObserver(window.debounce(() => {
            this.updateImageSizes();
         }, 250));
         this.resizeObserver = new ResizeObserver(() => {
            window.debouncer.schedule(
               'feed-update-images',
               () => this.updateImageSizes(),
               250
            );
         });
      } else {
         window.addEventListener('resize', window.debounce(()=> {
            this.updateImageSizes();
         }, 250));
         window.addEventListener('resize', () => {
            window.debouncer.schedule(
               'feed-update-images',
               () => this.updateImageSizes(),
               250
            );
         });
      }
      window.addEventListener('popstate', this.popStateHandler);
@@ -557,35 +725,6 @@
      document.addEventListener('change', this.changeHandler);
   }
   loadImage(img) {
      const src = this.getAppropriateImageSize(img);
      if (src && src !== img.src) {
         img.src = src;
         img.dataset.loaded = 'true';
      }
   }
   getAppropriateImageSize(img) {
      const width = window.innerWidth;
      if (width < 768 && img.dataset.small) {
         return img.dataset.small;
      } else if (img.dataset.medium) {
         return img.dataset.medium;
      }
      return img.src; // Fallback to current src
   }
   observeImages(container) {
      const images = container.querySelectorAll('img[data-small], img[data-medium]');
      images.forEach(img => {
         if (!img.dataset.loaded) {
            this.imageObserver.observe(img);
         }
      });
   }
   handlePopState(e) {
      if (e.state?.filters) {
         if (this.processURLFilters()) {
@@ -654,6 +793,39 @@
   }
}
document.addEventListener('DOMContentLoaded', function() {
   window.feedBlock = new FeedBlock();
document.addEventListener('DOMContentLoaded', async function() {
   window.auth.subscribe(event => {
      if (event === 'auth-loaded') {
         window.feedBlock = new FeedBlock();
      }
   });
   let item = {
      content: "art",
      date: "2025-12-24 03:37:26",
      fields: {
         gallery: "",
         post_content: "",
         post_thumbnail: 200,
         post_title: "Great Gray Owl",
         price: "",
      },
      icon: "arrows-clockwise",
      id: 195,
      images: {
         200: {
            'image-alt-text': "",
            'image-caption': "",
            'image-title': "Great Gray Owl",
            large: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl.jpg",
            medium: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-1024x1024.jpg",
            small: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-300x300.jpg",
            tiny: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-50x50.jpg"
         }
      },
      url: "http://jakevan.test/art/great-gray-owl/",
      user_id: 3
   };
});