Jake Vanderwerf
9 days ago ed57c386db34d8693ca75311972d0929ebe5f488
src/feed/viewOld.js
@@ -1,745 +1,770 @@
class FeedBlock {
class FeedBlockOld {
   constructor() {
      this.cache = window.jvbCache;
      this.a11y = window.jvbA11y;
      this.loading = window.jvbLoading;
      this.error = window.jvbError;
      this.container = document.querySelector('section.feed-block');
      if (!this.container) {
         return;
      }
      this.openGallery = false;
      this.a11y = window.jvbA11y;
      this.cache = new window.jvbCache('feed');
      this.error = window.jvbError;
      this.initElements();
      this.addPlaceholders();
      this.config = {
         api: feedSettings.apiUrl,
         nonce: feedSettings.nonce,
         user: jvbSettings.currentUser || null,
         source: '',
         context: '',
         highlight: null,
         gallery: false,
         showAuthor: true,
         showDate: false,
         view: localStorage.getItem('feedViewMode') || 'grid',
         view: this.cache.get('feedView') || 'grid',
         ... this.container.dataset
      };
      this.taxonomies = {};
      this.rendered = {};
      this.initElements();
      this.initFilters();
      this.feed = {
         imageLoadThreshold: 5,
         lazyLoadOffset: '100px',
         gallery: [],
         loaded: 0,
         intsersectionObserver: null,
         templates: new Map()
      };
      this.isLoading = false;
      this.hasMore = true;
      this.retries = {
         count: 0,
         max: 3,
         delay: 1000
      };
      this.page = 1;
      this.order = 'DESC';
      this.orderby = 'date';
      this.gallery = (this.config.gallery) ? new window.jvbGallery(document.querySelector('dialog.gallery'), {
         imageWrapper: '.item',
         loadMore: ()=>this.fetchFeed.bind(this)
      }) : false;
      this.initListeners();
      if (this.page === 1) {
         this.processURLFilters();
      } else {
         this.updateFilters();
      }
      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.filterSelector = 'form.feed-filters';
      this.filterForm = this.container.querySelector(this.filterSelector);
      this.grid = this.container.querySelector('.item-grid');
      this.loadMore = this.container.querySelector('.load-more');
      this.filterControls = this.container.querySelector('.filter-actions');
      this.contentTypes = Array.from(this.filterForm.querySelectorAll('input[name="content"]')).map(
         content => {
            return content.value;
         });
      this.selectedTerms = this.container.querySelector('.selected-items-section .selected-items');
      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 = [];
      }
   }
   initListeners() {
      window.addEventListener('popstate', this.handlePopState.bind(this));
      document.addEventListener('click', this.handleClick.bind(this));
      document.addEventListener('change', this.handleChange.bind(this));
   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);
      // Intersection observer for lazy loading
      if ('IntersectionObserver' in window) {
         this.imageObserver = new IntersectionObserver(entries => {
            entries.forEach(entry => {
               if (entry.isIntersecting) {
                  this.loadImage(entry.target);
                  this.imageObserver.unobserve(entry.target);
               }
            });
         }, {
            rootMargin: '100px',
            threshold: 0.1
         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;
            }
         });
      }
      // Resize observer for responsive images
      if ('ResizeObserver' in window) {
         this.resizeObserver = new ResizeObserver(window.debounce(() => {
            this.updateImageSizes();
         }, 250));
         // Observe the container
         this.resizeObserver.observe(this.container);
      } else {
         // Fallback to window resize
         window.addEventListener('resize', window.debounce(() => {
            this.updateImageSizes();
         }, 250));
      }
      // Update content-specific visibility
      this.updateContentFor(this.filters.content);
   }
   nextPage() {
      this.store.setFilter('page', this.store.filters.page++);
   }
      this.taxonomies = {};
      this.container.querySelectorAll('.jvb-selector:not([hidden])').forEach(selector => {
         let taxonomy = selector.dataset.taxonomy;
         if (!Object.hasOwn(this.taxonomies, taxonomy)) {
            this.taxonomies[taxonomy] = new window.jvbTaxonomySelector(
               selector,
               {
                  multiple: true,
                  feed: true,
                  selected: {},
                  onClose: () => this.setSelectedTerms(taxonomy),
   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;
         }
      });
   }
   /**
    * Handle browser history navigation
    */
   handlePopState(e) {
      if (e.state && e.state.filters) {
         if(this.processURLFilters()){
            // Load items with updated filters
            this.resetPage();
            this.fetchFeed();
            // Announce to screen readers
            this.a11y.announce('Feed filters updated from browser history.');
         }
   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() {
      const params = new URLSearchParams(window.location.search);
      //No parameters to process
      if (!params.toString()) {
         this.updateFilters();
         return;
      if (this.filters.page > 1) {
         return false;
      }
      const params = new URLSearchParams(window.location.search);
      let filters = ['content', 'order', 'orderby', 'favourites','match'];
      if (!params.toString()) {
         return false;
      }
      let filters = ['content', 'order', 'orderby', 'favourites', 'match'];
      filters.forEach(filter => {
         let value = params.get('f_'+filter);
         params.delete('f_'+filter);
         if (value && this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`)) {
            this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`).checked = true;
         let value = params.get(`f_${filter}`);
         if (value) {
            this.filters[filter] = value;
            let input = this.ui.filters[filter];
            if (input) {
               input.checked = true;
            }
         }
      });
      let unprocessed = {};
      for (var [key, value] of Object.entries(Object.fromEntries(params))) {
         key = key.replace('f_','');
         if (this.contentTypes.includes(key)) {
            this.openGallery = value;
         } else {
            this.taxonomies[key].addTermsFromURL(value);
            this.setSelectedTerms(key);
      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);
         }
      }
      this.updateFilters();
   }
   handleClick(e) {
      if (e.target.classList.contains('load-more') || e.target.closest('.load-more')) {
         this.fetchFeed(false);
         e.target.disabled = true;
      } else if (e.target.classList.contains('clear-filters') || e.target.closest('.clear-filters')) {
         this.resetFilters();
      } else if (this.config.gallery && e.target.closest('.feed-image')) {
         this.gallery.handleGalleryOpen(e);
      } else if (e.target.classList.contains('.remove-item') || e.target.closest('.remove-item')) {
         let tag = e.target.closest('.selected-item');
         let taxonomy = tag.dataset.taxonomy;
         this.taxonomies[taxonomy].removeSelectedTerm(tag.dataset.id);
         this.setSelectedTerms(taxonomy);
         this.updateFilters();
      }
   }
   handleChange(e) {
      if (e.target.closest(this.filterSelector)) {
         this.resetPage();
         window.removeChildren(this.grid);
         this.addPlaceholders();
         //update filters
         this.updateFilters();
      }
   }
   updateFilters() {
      this.page = 1;
      const params = new URLSearchParams(window.location.search);
      let filters = Object.fromEntries(new FormData(this.filterForm));
      let contents = [];
      for (let [key, value] of Object.entries(filters)) {
         let set = false;
         switch (key) {
            case 'content':
               if (value !== this.contentTypes[0]) {
                  set = true;
      });
      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 {
                  params.delete('f_'+key);
                  this.selector.registerField(button, {
                     button: button,
                     buttonSelector: '[data-filter="taxonomy"]',
                     selected: this.ui.selectedTax,
                     selectedItems: ids
                  });
               }
               break;
            case 'orderby':
               if (value !== 'date') {
                  set = true;
               }
               break;
            case 'order':
               if (value !== 'desc') {
                  set = true;
               }
               break;
            default:
               set = true;
            }
         }
         if (!set) {
            params.delete('f_'+key);
         }
         if (set && value !== false && value !== '') {
            params.set('f_'+key, value);
         }
         if (value !== '') {
            contents.push(value);
         }
         const newURL = `${window.location.pathname}?${params.toString()}`;
         history.pushState(filters, '', newURL);
      }
      this.filters = filters;
      this.updateContentFor(filters.content);
      this.updateFilterControls();
      this.loading.setContent(contents);
      this.fetchFeed(true);
   }
   updateFilterControls() {
      this.filterControls.hidden = this.selectedTerms.children.length < 2;
      return true;
   }
   /**
    * Toggles taxonomy selectors and certain order/orderby options
    *  depending on current content
    * @param content
    * Update URL with current filters (for sharing/bookmarking)
    */
   updateContentFor(content) {
      this.filterForm.querySelectorAll('.jvb-selector').forEach(tax => {
         let hasContent = tax.dataset.for.includes(content);
         tax.hidden = !hasContent;
         if (!hasContent) {
            let t = tax.dataset.taxonomy;
            this.clearSelectedTerms(t);
   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]);
         }
      });
      this.filterForm.querySelectorAll('input[data-for]').forEach(toggle => {
         toggle.hidden = !toggle.dataset.for.includes(content);
      // Add taxonomy filters
      Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => {
         if (terms.length > 0) {
            params.set(`f_tax_${taxonomy}`, terms.join(','));
         }
      });
      this.filterForm.querySelectorAll('input[name="order"]').forEach(order => {
         order.hidden = this.filters.order === 'random';
      });
      // Update URL without reload
      const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
      window.history.pushState({ filters: this.filters }, '', newURL);
   }
   clearSelectedTerms(taxonomy) {
      this.filterForm.querySelector(`input[name="${taxonomy}"]`).value = '';
      if (Object.hasOwn(this.taxonomies, taxonomy)) {
         this.taxonomies[taxonomy].selectedItems = {};
      }
   }
   setSelectedTerms(taxonomy) {
      let input = this.filterForm.querySelector(`input[name="${taxonomy}"]`);
      input.value = '';
      let selected = this.taxonomies[taxonomy].selectedTerms;
      if (!window.isEmptyObject(selected)) {
         let ids = Object.keys(selected);
         input.value = ids.join(',');
      }
      this.updateFilters();
   }
   nextPage() {
      if (this.hasMore) {
         this.page++;
      }
   }
   resetPage() {
      this.page = 1;
      this.hasMore = true;
   }
   resetState() {
      this.resetPage(true);
      this.isLoading = false;
      this.retries = {
         count: 0,
         max: 3,
         delay: 1000
      };
   }
   resetFilters() {
      this.filterForm.reset();
      //check the first content
      this.filterForm.querySelector('input[name="content"]').checked = true;
      this.filterForm.querySelector('input[name="orderby"][value="date"]').checked = true;
      this.page = 1;
      this.updateFilters();
   }
   buildFilterRequest() {
      let filters = {};
      for (let [filter, value] of Object.entries(this.filters)) {
         if (value !== false && value !== '') {
            filters[filter] = value;
         }
      }
      filters.page = parseInt(this.page);
      if (this.container.dataset.context) {
         filters.context = this.container.dataset.context;
      }
      if (this.container.dataset.source) {
         filters.source = this.container.dataset.source;
      }
      return new URLSearchParams(filters).toString();
   }
   async fetchFeed(reset = false, force = false) {
      if (this.isLoading) {
         return false;
      }
      this.loading.showLoading(this.filters);
      try {
         if (this.page === 1) {
            window.removeChildren(this.grid);
            this.addPlaceholders();
         }
         const data = await this.cache.fetchWithCache(
            `${this.config.api}feed?${this.buildFilterRequest()}`,
            {
               method: 'GET',
            },
            {
               context: 'feed',
               forceRefresh: true
               // forceRefresh: force
            }
         );
         //Handle empty results
         if (!data || !data.items || data.items.length === 0) {
            if (this.page === 1) {
               this.showEmptyState();
            }
            this.hasMore = false;
            return false;
         } else {
            this.hasMore = data['has_more'];
            this.renderItems(data.items, this.page > 1);
            if (this.hasMore) {
               this.nextPage();
            }
            return true;
         }
      } catch (error) {
         this.handleError(error);
      } finally {
         this.loading.hideLoading();
         if (this.openGallery !== false) {
            this.gallery.openWhenReady = this.openGallery;
            this.openGallery = false;
         }
         this.loadMore.disabled = false;
         this.loadMore.hidden = !this.hasMore;
      }
   }
   removePlaceholders() {
      if (this.grid.querySelector('.placeholder')) {
         window.removeChildren(this.grid);
      }
   }
   showEmptyState() {
      window.removeChildren(this.grid);
      let template = window.getTemplate('emptyState');
      let isFavourite = Object.hasOwn(this.filters, 'favourites') && this.filters.favourites === true;
      if (isFavourite) {
         [
            template.querySelector('h3').textContent,
            template.querySelector('p:first-of-type').textContent,
            template.querySelector('p:last-of-type').textContent,
         ] = [
            '♡ BLANK CANVAS â™¡',
            'You haven\'t fallen in love with any pieces... yet!',
            'Hit that heart icon when something stops your scroll â€” your dream collection is waiting to start.'
         ];
      }
      this.grid.append(template);
      this.a11y.announceEmpty(isFavourite);
   }
   handleError(error){
      return this.error.handleApiError(
         error,
         {
            component: 'Feed Block',
            action: 'loaditems'
         },
         () => this.fetchFeed()
      );
   }
   addPlaceholders() {
      let total = this.contentTypes.length - 1;
      for (let i = 0; i < 9; i++) {
         let template = window.getTemplate('placeholderTemplate');
         let rand =  Math.floor(Math.random()*total+1);
         let icon = window.getIcon(this.contentTypes[rand]).cloneNode(true);
         template.append(icon);
         this.grid.append(template);
      }
   }
   renderItems(items, append = false) {
      //Clear the grid if we aren't appending
      if (!append) {
         window.removeChildren(this.grid);
         this.addPlaceholders();
   renderItems() {
      let items = this.store.getFiltered();
      if (this.store.filters['page'] === 1) {
         window.removeChildren(this.ui.grid);
      }
      //Bail early if no items
      if (items.length === 0) {
         this.a11y.announceUpdate(0, append);
         this.a11y.announceItems(0, this.store.filters['page'] > 0);
         return;
      }
      //Use DocumentFragment for better performance
      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);
            this.imageObserver.observe(element);
            fragment.appendChild(element);
         }
         if (endIndex < items.length) {
            requestAnimationFrame(() => {
               processBatch(endIndex);
            });
            requestAnimationFrame(() => processBatch(endIndex));
         } else {
            this.removePlaceholders();
            //all batches are processed, append fragment
            this.grid.appendChild(fragment);
            this.ui.grid.append(fragment);
            if (this.config.gallery) {
               this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
            }
            this.a11y.makeNavigable(this.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
            this.a11y.announceItems(items.length, append, this.hasMore);
            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.announceUpdate(0, append);
         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;
      }
   }
   /**
    * Creates a feed-item. Used by RenderItems
    *
    * @param {object} item
    */
   createItemElement(item) {
      if(!this.rendered[item.icon]) {
         this.rendered[item.icon] = new Map();
      }
      if (this.rendered[item.icon].has(item.id)) {
         return this.rendered[item.icon].get(item.id);
      }
      let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
      const favourited = window.isFavourited(item.icon, item.id)??false;
      const template = window.getTemplate('feed-item');
      const isTimeline = Object.hasOwn(template.dataset, 'timeline');
      template.id = `${item.icon}-${item.id}`;
      template.dataset.id = item.id;
      template.classList.add(item.icon);
      // 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 (item['umami_view']) {
         this.buildUmamiData(template, item['umami_view']);
      }
         if (value === '') {
            el.remove();
            continue;
         }
      let favouriteButton = template.querySelector('button.favourite');
      [
         favouriteButton.dataset.id,
         favouriteButton.dataset.type,
         favouriteButton.dataset.artist,
         favouriteButton.title
      ] = [
         item.id,
         item.icon,
         item['user_id'],
         (favourited) ? 'Remove from Favourites' : 'Add to Favourites'
      ];
      let order = item.order;
      let single = template.querySelector('.item');
      let list = template.querySelector('.item-list');
      let img = template.querySelector('.feed-images');
      let summary = template.querySelector('summary');
      let info = template.querySelector('.item-info');
      for (let [index, id] of Object.entries(order)) {
         let target;
         let config = item[id];
         if (id === 'title') {
            target = template.querySelector('h3 a');
            if (item.title !== '') {
               [
                  target.textContent,
                  target.href,
                  target.url
               ] = [
                  item.title,
                  item.url,
                  `Learn more about this ${item.icon}`
               ];
               if (item.icon !== '') {
                  target.closest('h3').prepend(window.getIcon(item.icon));
               }
               if (item.umami_click) {
                  this.buildUmamiData(target, item.umami_click);
               }
            } else {
               target.remove();
            }
         }  else if (Object.hasOwn(config, 'terms')) {
            //Taxonomy list
            if (config.terms.length === 0) {
               continue;
            }
            let taxonomy = list.cloneNode(true);
            let label = taxonomy.querySelector('.label');
            let termList = taxonomy.querySelector('ul');
            let listItem = taxonomy.querySelector('li');
            if (config.label) {
               label.textContent = config.label;
            }
            if (config.icon) {
               label.prepend(window.getIcon(config.icon));
            }
            if (!config.label && !config.icon){
               label.remove();
            }
            config.terms.forEach(term => {
               let termItem = listItem.cloneNode(true);
               let link = termItem.querySelector('a');
               [
                  link.href,
                  link.title,
                  link.textContent
               ] = [
                  term.url,
                  `Learn more about ${term.title}`,
                  term.title
               ];
               if (term.umami_click.length > 0) {
                  this.buildUmamiData(link, term.umami_click);
               }
               termList.append(termItem);
            });
            listItem.remove();
            info.appendChild(taxonomy);
         } else if (Object.hasOwn(config, 'value') && config.value !== '') {
            let itemInfo = single.cloneNode(true);
            let label = itemInfo.querySelector('.label');
            let link = itemInfo.querySelector('a');
            let p = itemInfo.querySelector('p');
            if (Object.hasOwn(config, 'label')) {
               label.textContent = config.label;
            }
            if (Object.hasOwn(config, 'icon')) {
               label.prepend(window.getIcon(config.icon));
            }
            if (!Object.hasOwn(config, 'icon') && !Object.hasOwn(config, 'label')) {
               label.remove();
            }
            if (Object.hasOwn(config, 'url')) {
               p.remove();
               [
                  link.textContent,
                  link.href,
                  link.title
               ] = [
                  config.value,
                  config.url,
                  `Learn more about ${config.value}`
               ];
            } else {
               link.remove();
               p.textContent = config.value;
            }
            info.appendChild(itemInfo);
         } else if (id === 'image') {
            let images = summary.querySelector('.feed-images');
            let img = images.querySelector('a');
            let main = img.cloneNode(true);
            if (!this.config.gallery) {
               main.href = item.url;
            }
            main.classList.add('feed-image');
            this.buildImageData(main.querySelector('img'), item.image);
            images.append(main);
            if (item.content?.length > 0) {
               images.classList.add('multi');
               item.content.forEach(c => {
                  let image = img.cloneNode(true);
                  if (!this.config.gallery) {
                     image.href = c.url;
                  }
                  let itemImg = image.querySelector('img');
                  itemImg.src = c.image.small;
                  itemImg.alt = c.image.alt;
                  images.append(image);
               });
            }
            img.remove();
         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);
         }
      }
      single.remove();
      list.remove();
      this.rendered[item.icon].set(item.id, template);
      // 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;
   }
   buildImageData(img, data){
      if (typeof data.tiny !== 'string') {
         return;
   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;
      [
         img.src,
         img.dataset.small,
         img.dataset.medium,
         img.dataset.large,
         img.alt
      ] =
         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;
         [
            data.tiny,
            data.small,
            data.medium,
            data.large,
            data.alt
            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;
   }
   buildUmamiData(item, data){
      for(let [key, value] of Object.entries(data)){
         item.dataset[key] = 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']);
      }
   }
   /**
    * Load Image, used by renderItems
    * @param element
    */
   loadImage(element) {
      const img = element.querySelector('img');
      if (!img) return;
      const size = this.getImageSize();
      img.src = img.dataset[size] || img.dataset.src;
      element.setAttribute('data-loaded', 'true');
   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);
   }
   /**
    * Updates the image size according to screen size
    *
    * @param {object} filters {name: value}
    */
   updateImageSizes() {
      const size = this.getImageSize();
      const items = this.grid.querySelectorAll('.item');
      items.forEach(item => {
         const img = item.querySelector('img');
         if (img && img.dataset[size] && img.src !== img.dataset[size]) {
            img.src = img.dataset[size];
   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;
            }
         }
      });
   }
   /**
    * Get image size based on screen width
    */
   getImageSize() {
      const width = window.innerWidth;
      if (width > 1024) return 'medium';
      if (width > 500) return 'medium';
      return 'small';
      // 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', () => {
   window.feedBlock = new FeedBlock();
document.addEventListener('DOMContentLoaded', async function() {
   window.auth.subscribe(event => {
      if (event === 'auth-loaded') {
         window.feedBlock = new FeedBlock();
      }
   });
});