Jake Vanderwerf
2026-02-17 a24a06002081ad71a78ffeff9072725ba39cf121
src/feed/view.js
@@ -1,13 +1,12 @@
class FeedBlock {
   constructor() {
      this.container = document.querySelector('section.feed-block');
      if (!this.container) {
         return;
      }
      if(!this.container) return;
      this.a11y = window.jvbA11y;
      this.cache = new window.jvbCache('feed');
      this.error = window.jvbError;
      this.cache = new window.jvbCache('feed');
      this.templates = window.jvbTemplates;
      this.config = {
         source: '',
@@ -17,187 +16,438 @@
         view: this.cache.get('feedView') || 'grid',
         ... this.container.dataset
      };
      this.init();
   }
   init() {
      this.initElements();
      this.defineTemplates();
      this.initListeners();
      this.initFilters();
      this.loadWhenAble();
   }
   loadWhenAble() {
      if ('requestIdleCallback' in window) {
         requestIdleCallback(() => {
            this.initTaxonomies();
            this.initStore();
            this.initListeners();
            this.initTaxonomies();
            this.processCachedFilters();
            this.processURLFilters();
            this.updateFilterUI();
            this.initGallery();
         }, { timeout: 2000 });
      } else {
         setTimeout(() => {
            this.initTaxonomies();
            this.initStore();
            this.initListeners();
            this.initTaxonomies();
            this.processCachedFilters();
            this.processURLFilters();
            this.updateFilterUI();
            this.initGallery();
         }, 100);
      }
   }
   initElements() {
      this.currentTaxonomies = new Set();       // Allowed Taxonomies, grabbed from active buttons
      this.taxonomyFilters = {};
      this.elements = {
      this.selectors = {
         filterTrigger: '[data-filter]',
         filters: {
            content: '[data-filter="content"]',
            orderby: '[data-filter="orderby"]',
            order: '[data-filter="order"]',
            match: '[data-filter="match"]',
            actions:    '.filter-actions .toggle-text',
            container:  '.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"]'
            taxonomy:   '[data-filter^="taxonomy"]',
         },
         selectedTax: '.selected-items',
         clearFilter: 'button.clear-filters',
         loadMore: 'button.load-more',
         filterContainer: '.filters',
         grid: '.item-grid',
         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.elements);
      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.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']];
      }
      this.orderbyFilters = (this.ui.orderby)
         ? Array.from(this.ui.orderby).map(o => o.value)
         : [];
      if (this.ui.taxonomies.length>0) {
         this.taxonomies = Array.from(
            this.ui.taxonomies,
         ).map(content => content.dataset.taxonomy);
      } else {
         this.taxonomies = [];
      }
      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)
         : [];
   }
   async initTaxonomies() {
      this.selector = window.jvbSelector;
      const buttons = document.querySelectorAll('[data-filter="taxonomy"]');
   initListeners() {
      this.popStateHandler = this.handlePopState.bind(this);
      this.clickHandler    = this.handleClick.bind(this);
      this.changeHandler      = this.handleChange.bind(this);
      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
      });
      window.addEventListener('popstate', this.popStateHandler);
      document.addEventListener('click', this.clickHandler);
      document.addEventListener('change', this.changeHandler);
   }
   initFilters() {
      //defaults
      this.filters = {
         content:    this.contentTypes[0],
         orderby:    'date',
         order:      'desc',
         page:       1
      this.allowedFilters = ['content', 'order', 'orderby', 'favourites', 'match'];
      let defaults = {
         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;
      if (this.config.context) defaults.context = this.config.context;
      if (this.config.source) defaults.source = this.config.source;
      //check the cache
      this.processCachedFilters();
      //check url
      this.processURLFilters();
      // Set initial UI state
      this.syncUIToFilters();
      this.filters = defaults;
      this.defaults = {...defaults};
   }
   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;
   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 isHidden = Object.keys(this.taxFilters).length === 0;
      if (this.ui.buttons.clearFilters) {
         this.ui.buttons.clearFilters.hidden = isHidden;
      }
      if (this.ui.filters.actions) {
         this.ui.filters.actions.hidden = isHidden;
      }
   }
   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();
            }
         });
      }
      // Update content-specific visibility
      this.updateContentFor(this.filters.content);
   }
   nextPage() {
      this.store.setFilter('page', this.store.filters.page++);
   }
   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',
         {
@@ -208,301 +458,136 @@
               { 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,
            TTL: 6 * 60 * 60 * 1000, //6 hours
            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'];
               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;
         }
      });
   }
   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();
               }
            }
         });
      }
   isFirstPage() {
      return this.store.filters.page === 1;
   }
   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) {
   renderItems(items = null) {
      items = items??this.store.getFiltered();
      if (this.isFirstPage()) {
         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);
         this.showEmptyState();
         this.a11y.announceItems(0, this.isFirstPage());
      } else {
         this.a11y.announceItems(0, this.store.filters['page'] >1, false);
         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(()=>{});
      }
      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;
      }
      this.updateFilterControls();
   }
   /**
    *
    * @param {object} item
    */
   showEmptyState() {
      window.removeChildren(this.ui.grid);
      this.ui.grid.append(this.templates.create('emptyState'));
   }
   createItemElement(item) {
      let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
      if (Object.hasOwn(template.dataset, 'timeline')) {
         return this.createTimelineElement(item, template);
      if (typeof item !== 'object') {
         item = this.store.get(item);
         if (!item) return;
      }
      //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;
      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) {
         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.hasOwn(item.images, v)) {
            console.log('Item.images does not have id');
            return true;
         }
      });
      return false;
      return values.some(v =>
         Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v))
      );
   }
   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 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;
         value.forEach(imgID => {
         values.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);
         this.formatImageField(element, values[0], item);
      }
   }
      formatImageField(element, value, item) {
         [
            element.src,
            element.srset,
            element.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']
         ]
      }
   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;
      }
      Object.keys(item.taxonomies).forEach(taxonomy => {
         if (taxonomy === field) {
            return true;
         }
      });
      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;
@@ -511,14 +596,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);
      }
@@ -533,33 +620,13 @@
         if (!element) return;
      }
      element.setAttribute('datetime', value);
      element.textContent = window.formatTimeAgo(value);
      element.textContent = window.formatTimeAgo(value, 'F Y');
   }
   formatField(element, value) {
      element.textContent = value;
      element.textContent = window.decodeHTMLEntities(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);
            }
         }
      }
   addTimelineElements(item, template) {
      let [
         afterEl,
         number,
@@ -571,11 +638,12 @@
         template.querySelector('[data-field="started"] time'),
         template.querySelector('[data-field="updated"] time')
      ];
      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']);
@@ -583,8 +651,6 @@
      if (last) {
         this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']);
      }
      return template;
   }
   removePlaceholders() {
@@ -594,198 +660,82 @@
      }
   }
   defineTemplates() {
      const T = this.templates;
      const f = this;
   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);
      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);
         }
         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);
      });
      T.define('emptyState');
      // 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;
      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);
               }
            }
         }
      });
      // 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 });
         }
      }
   }
   // 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() {
@@ -794,33 +744,4 @@
         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
   };
});