Jake Vanderwerf
5 days ago a9b3b28d001941921aa70d37fdc87c758a163a44
src/feed/viewOld.js
@@ -1,1657 +1,770 @@
class FeedBlock {
    static LOADING_QUIPS = JSON.parse(feedSettings.quips);
    constructor(container){
        this.cache = window.jvbCache;
        this.eventHandlers = new Map();
        this.rendered = {};
        // Store container references
        this.container = container;
        this.a11y = window.jvbA11y;
        //For tracking cache and current requests
        this.currentRequest = null;
        this.timeoutId = null;
        // Initialize Config
        this.initConfig();
        // Setup error handler
        this.error = window.jvbError;
        this.resetState();
        this.state.firstLoad = false;
        this.state.URLProcessed = false;
        this.highlightGot = false;
        this.selectorInstances = {};
        this.elements = {
            filters: this.container.querySelector('form.feed-filters'),
            selected: this.container.querySelector('.selected-items'),
            clearFilters: this.container.querySelector('button.clear-filters'),
            grid: this.container.querySelector('.feed-grid'),
            loadMore: this.container.querySelector('button.load-more'),
            spinner: this.container.querySelector('.loading-spinner'),
            loading: this.container.querySelector('.feed-overlay'),
            matchAll: this.container.querySelector('.filter-actions .toggle-text'),
        }
        this.filters = {
            content: this.getCurrentContent(),
            taxonomies: {},
            favourites: false,
            orderby: 'date',
            order: 'desc',
        }
        this.feed = {
            imageLoadThreshold: 5,
            lazyLoadOffset: '100px',
            gallery: [],
            loaded: 0,
            intersectionObserver: null,
            templates: new Map()
        }
        this.imageObserver = null;
        this.resizeObserver = null;
        this.highlight = null;
        //Loading settings
        this.loadingIndex = 0;
        this.quips = this.initializeQuips();
        this.loadingMessage = this.container.querySelector('.loading-message');
        this.dotsElement = this.container.querySelector('.loading-dots');
        this.quipInterval = null;
        this.loadingOptions = {
            loadingMessages: {},
            cycleInterval: 2000, //time between loading messages
        }
        this.initFilters();
        if(this.container.dataset.gallery){
            this.initGallery();
            this.setupGalleryAccessibility();
        }
        this.initListeners();
        if(!this.state.URLProcessed){
            this.updateFilters();
        }
        this.selectedListeners = this.checkSelectedClicks.bind(this);
        this.updateSelectedListeners();
    }
    initConfig() {
        // Get settings from container data attribute
        const settings = JSON.parse(this.container.dataset.settings || '{}');
        let content = Array.from(this.container.querySelectorAll('input[name="content"]')).map(content=> content.value);
        this.config = {
            api: feedSettings.apiUrl,
            nonce: feedSettings.nonce,
            currentUser: jvbSettings.currentUser || null,
            content: content[0],
            contentTypes: content,
            taxonomies: Array.from(this.container.querySelectorAll('.jvb-selector')).map(taxonomy => taxonomy.dataset.taxonomy),
            // Source information for analytics
            source: this.container.dataset.source || '',
            context: this.container.dataset.context || '',
            // Optional highlight
            highlight: null,
            // Gallery mode
            isGallery: this.container.dataset.gallery || false,
            showAuthor: !this.container.dataset.gallery || true,
            showDate: this.container.dataset.gallery || false,
            // User preferences
            viewMode: localStorage.getItem('feedViewMode') || 'grid',
        }
        if(settings.isGallery){
            this.config.highlight = this.getHighlight();
        }
    }
    initFilters(){
        this.updateContentFor(this.getCurrentContent());
    }
    checkSelectedClicks(e){
        if(e.target.closest('.remove-item')){
            let tag = e.target.closest('.selected-item');
            // Uncheck checkbox if it exists
            let taxonomy = tag.dataset.taxonomy;
            this.clearSelectedTerm(tag.dataset.id, taxonomy);
            // Remove tag
            tag.remove();
            // Update clear filters button visibility
            this.updateClearFiltersButton();
            this.updateFilters();
            this.updateSelectedListeners();
        }
    }
    updateSelectedListeners(){
        this.elements.selected.removeEventListener('click', this.selectedListeners);
        if(this.elements.selected.children.length>0){
            this.elements.selected.addEventListener('click', this.selectedListeners);
        }
    }
    handleFilterChange(e){
        if(e.target.closest('.jvb-selector')){
            return;
        }
        this.resetPage();
        if(e.target.name === 'content'){
            let content = e.target.value;
            this.updateContentFor(content);
        }
        this.updateFilters();
    }
    handleLoadMore(){
        if (this.state.loading || !this.state.hasMore) {
            return;
        }
        this.fetchFeed();
    }
    initListeners(){
        window.addEventListener('popstate', this.handlePopState.bind(this));
        this.addEvent(this.elements.filters, 'change', (e) => this.handleFilterChange(e));
        this.addEvent(this.elements.filters, 'submit', e => e.preventDefault());
        this.addEvent(this.elements.loadMore, 'click', () => this.handleLoadMore());
        this.addEvent(this.elements.clearFilters, 'click', () => this.clearSelectedTaxonomies());
        if(this.config.isGallery){
            this.addEvent(this.elements.grid, 'click', (e) =>this.handleGalleryOpen(e));
        }
        // 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: this.feed.lazyLoadOffset,
                threshold: 0.1
            });
        }
        // 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));
        }
    }
    handleGalleryOpen(e){
        const feedImage = e.target.closest('.feed-image');
        if(feedImage){
            const item = feedImage.closest('.feed-item');
            if(item) {
                const index = Array.from(this.container.querySelectorAll('.feed-item')).indexOf(item);
                if(index !== -1){
                    this.openGallery(index);
                }
            }
        }
    }
    initGallery(){
        this.gallery = {
            items: this.getGalleryItems() || [],
            index: 0,
            touchStart: null,
            touchEnd: null,
            minSwipe: 50,
            modal: this.createGalleryModal(),
            keyHandler: null,
            loading: false
        }
        document.body.appendChild(this.gallery.modal);
    }
    /**
     * 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.');
            }
        }
    }
    processURLFilters(){
        const params = new URLSearchParams(window.location.search);
        if (!params.toString()) {
            return false; // No parameters to process
        }
        // Initialize content type
        const content = params.get('f_content');
        params.delete('f_content');
        if (content && this.elements.filters.querySelector(`input[value="${content}"]`)) {
            this.elements.filters.querySelector(`input[value="${content}"]`).checked = true;
            this.filters.content = content;
        }
        // Initialize order
        const order = params.get('f_order');
        params.delete('f_order');
        if (order && this.elements.filters.querySelector(`input[name="order"][value="${order}"]`)) {
            this.elements.filters.querySelector(`input[name="order"][value="${order}"]`).checked = true;
            this.filters.order = order;
        }
        // Initialize orderby
        const orderby = params.get('f_orderby');
        params.delete('f_orderby');
        if (orderby && this.elements.filters.querySelector(`input[name="orderby"][value="${orderby}"]`)) {
            this.elements.filters.querySelector(`input[name="orderby"][value="${orderby}"]`).checked = true;
            this.filters.orderby = orderby;
        }
        // Initialize favourites filter
        if (params.get('f_favourites') === 'true' && this.config.currentUser !== null) {
            params.delete('f_favourites');
            const favCheckbox = this.elements.filters.querySelector(`input[name="favourites_only"]`);
            if (favCheckbox) favCheckbox.checked = true;
            this.filters.favourites = true;
        }
        // Initialize match, if present
        if(params.get('f_match') === 'all'){
            params.delete('f_match');
            this.elements.matchAll.querySelector('input').checked = true;
        }
        window.removeChildren(this.elements.selected);
        let filters = JSON.parse(JSON.stringify(this.filters));
        let unprocessed = {};
        for (var [key, value] of Object.entries(Object.fromEntries(params))) {
            if (key.startsWith('f_') ) {
                var taxName = key.replace('f_', '');
                let cache = this.cache.getItem(taxName+'List');
                if(!Object.keys(filters['taxonomies']).includes(taxName)){
                    filters.taxonomies[taxName] = {};
                }
                // Handle both single values and comma-separated values
                const termIds = value.includes(',') ? value.split(',') : [value];
                termIds.forEach(termId=>{
                    if(cache && cache.hasOwnProperty(termId)){
                        filters.taxonomies[taxName][termId] = cache[termId].name;
                        let tag = this.createFilterTag(taxName, termId, cache[termId].name);
                        this.elements.selected.appendChild(tag);
                    }else{
                        if(!unprocessed.hasOwnProperty(taxName)){
                            unprocessed[taxName] = [];
                        }
                        unprocessed[taxName].push(termId);
                    }
                });
            }
        }
        this.filters = filters;
        if(!isEmptyObject(unprocessed)){
            this.fetchTermDetails(unprocessed);
        }
        if(this.config.isGallery){
            this.getHighlight();
        }
        this.updateContentFor(content);
        this.updateSelectedListeners();
        return true;
    }
    async loadFromURL() {
        if(this.processURLFilters()){
            // Announce to screen readers
            this.a11y.announce('Feed filters updated from URL.');
        }
        return true;
    }
    //We already checked the cache, now fetch the remaining term information
    async fetchTermDetails(terms) {
        let params = new URLSearchParams(terms);
        // Otherwise fetch the term details
        try {
            // Format the URL - might need to adjust based on your API structure
            const url = `${this.config.api}terms/check?`+params.toString();
            const termData = await this.cache.fetchWithCache(
                url, {
                    method: 'GET',
                },
                {
            });
            for(const [taxonomy, terms] of Object.entries(termData.terms)){
                if(!this.filters.taxonomies.hasOwnProperty(taxonomy)){
                    this.filters.taxonomies[taxonomy] = {};
                }
                this.cache.setItem(taxonomy+'List', terms, taxonomy);
                for(const [termId, termData] of Object.entries(terms)){
                    this.filters.taxonomies[taxonomy][termId] = termData.name;
                    let tag = this.createFilterTag(taxonomy, termId, termData.name);
                    this.elements.selected.appendChild(tag);
                }
                this.updateSelectedListeners();
            }
        } catch (error) {
            console.error(`Error fetching term details for ${terms}:`, error);
        }
    }
    //State Management
    nextPage(hasMore = true){
        this.state.page++;
        this.state.hasMore = hasMore;
    }
    resetPage(hasMore = true){
        this.state.page = 1;
        this.state.hasMore = hasMore;
    }
    resetState() {
        this.state = {
            page: 1,
            loading: false,
            hasMore: true,
            retries: {
                count: 0,
                max: 3,
                delay: 1000
            }
        }
    }
    updateFilters(reset = true){
        let updated = false;
        let content = this.getCurrentContent();
        let favourites = this.getFavouritesOnly();
        let order = this.getCurrentOrder();
        let orderby = this.getCurrentOrderby();
        let taxonomies = this.getSelectedTaxonomies();
        if(this.filters.content !== content ||
            this.filters.favourites !== favourites ||
            this.filters.order !== order ||
            this.filters.orderby !== orderby ||
            this.filters.taxonomies !== taxonomies ){
            updated = true;
        }
        (this.filters.content !== content)??this.updateContentFor(this.filters.content);
        this.filters.content = content;
        this.filters.favourites = favourites;
        this.filters.order = order;
        this.filters.orderby = orderby;
        this.filters.taxonomies = taxonomies;
        if(this.state.firstLoad){
            this.updateURL();
        }else{
            this.state.firstLoad = true;
        }
        if(reset){
            this.resetPage();
        }
        this.fetchFeed();
    }
    getCurrentContent(){
        return this.elements.filters.querySelector('input[name="content"]:checked').value;
    }
    getFavouritesOnly(){
        return this.elements.filters.querySelector('input[name="favourites_only"]')?.checked ??false;
    }
    getCurrentOrder(){
        return this.elements.filters.querySelector('input[name="order"]:checked').value;
    }
    getCurrentOrderby(){
        return this.elements.filters.querySelector('input[name="orderby"]:checked').value;
    }
    getCurrentTaxonomies(){
        let taxonomies = this.filters.taxonomies;
        let out = {};
        if(!isEmptyObject(taxonomies)){
            for(var [tax, terms] of Object.entries(taxonomies)){
                if(!isEmptyObject(terms)){
                    out[tax] =  Object.values(terms);
                }
            }
        }
        return out;
    }
    updateContentFor(content){
        this.elements.filters.querySelectorAll('.jvb-selector').forEach(tax=> {
            tax.hidden = !tax.dataset.for.includes(content);
            //Ensure any selected Taxonomies are removed
            if(!tax.dataset.for.includes(content)){
                this.clearSelectedTerms(tax);
            }
            //TODO: Ensure clean up of filtered out window.jvbTaxonomySelector instances?
            //Maybe cache them?
            let taxonomy = tax.dataset.taxonomy;
            if(tax.dataset.for.includes(content)){
                if(!this.selectorInstances.hasOwnProperty(taxonomy)){
                    this.selectorInstances[taxonomy] = new window.jvbTaxonomySelector(tax, {
                        multiple: true,
                        feed: true,
                        selected: this.filters[taxonomy]??{},
                        onClose: () => this.setSelectedTerms(taxonomy)
                    });
                }
            }else if(this.selectorInstances.hasOwnProperty(taxonomy)){
                this.clearSelectedTerms(taxonomy);
                delete this.selectorInstances[taxonomy];
            }
        });
        this.elements.filters.querySelectorAll('input[data-for]').forEach(toggle=>{
            toggle.hidden = !toggle.dataset.for.includes(content);
        });
        this.elements.filters.querySelectorAll('input[name="order"]').forEach(order=>{
            order.hidden = this.filters.orderby === 'random';
        });
    }
    updateURL(){
        const params = new URLSearchParams();
        let taxonomies = this.filters.taxonomies;
        if(!isEmptyObject(taxonomies)){
            for(var [tax, terms] of Object.entries(taxonomies)){
                if(!isEmptyObject(terms)){
                    params.set('f_'+tax, Object.keys(terms));
                }
            }
        }
        // Clone to avoid modifying original
        let filters = JSON.parse(JSON.stringify(this.filters));
        delete filters.taxonomies;
        for(const [key, value] of Object.entries(filters)){
            if(value !== false){
                params.set('f_'+key, value);
            }
        }
        if(this.elements.matchAll.querySelector('input:checked')){
            params.set('f_match', 'all');
        }
        const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
        history.pushState({ filters }, '', newUrl);
    }
    /**
     * Feed Fetching
     * @param reset clear grid
     * @param force force cache busting
     * @returns {Promise<void>}
     */
    async fetchFeed(reset = false, force = false){
        if(this.state.loading){
            return;
        }
        this.updateLoading(true);
        try{
            // Track URL processing
            if(this.state.page === 1){
                reset = true;
                await this.loadFromURL();
            }
            const params = this.buildFilterRequest();
            if(this.elements.matchAll.querySelector('input:checked')){
                params.append('match', true);
            }
            params.append('page', this.state.page);
            params.append('source', this.config.source);
            params.append('context', this.config.context);
            if(this.config.highlight){
                params.append('highlight', JSON.stringify(this.config.highlight));
            }
            //fetch data with cache busting
            const data = await this.cache.fetchWithCache(
                `${this.config.api}feed?${params.toString()}`,
                {
                    method: 'GET',
                },
                {
                    context: 'feed',
                    forceRefresh: true,
                    // forceRefresh: force,
                }
            )
         console.log(data, 'Fetched data: ');
            //Clear grid on first page
            if(this.state.page === 1){
                window.removeChildren(this.elements.grid);
            }
            //Handle empty results
            if(!data || !data.items || data.items.length === 0){
                if(this.state.page === 1){
                    this.showEmptyState();
                }
                this.state.hasMore = false;
            }else{
                this.state.hasMore = data.hasMore;
                if(this.state.hasMore){
                    this.nextPage();
                }
                //Render items
                this.renderItems(data.items, this.state.page > 1);
            }
        }catch(error){
            this.handleError(error);
        }finally{
            this.updateLoading(false);
            this.elements.loadMore.hidden = !this.state.hasMore;
        }
    }
    buildFilterRequest(){
        // Clone to avoid modifying original
        const filters = JSON.parse(JSON.stringify(this.filters));
        if(filters.taxonomies && !isEmptyObject(filters.taxonomies)){
            let temp = {};
            for(var [tax, terms] of Object.entries(filters.taxonomies)){
                if(!isEmptyObject(terms)){
                    temp[tax] =Object.keys(terms);
                }
            }
            delete filters.taxonomies;
            if(!isEmptyObject(temp)){
                filters.taxonomies =   JSON.stringify(temp);
            }
        }else{
            delete filters.taxonomies;
        }
        if(!filters.favourites){
            delete filters.favourites;
        }
        return new URLSearchParams(filters);
    }
    handleError(error){
        return this.error.handleApiError(
            error,
            {
                component: 'Feed Block',
                action: 'loaditems'
            },
            () => this.fetchFeed()
        );
    }
    getHighlight() {
        if(!this.config.highlight){
            const searchParams = new URLSearchParams(window.location.search);
            // Check for content type parameters
         this.config.contentTypes.forEach(type => {
            if (searchParams.has(type)) {
               this.config.highlight = {};
               this.config.highlight[type] = searchParams.get(type);
            }
         });
        }
        return this.config.highlight;
    }
    /**
     * Loading
     */
    updateLoading(loading) {
        this.state.loading = loading;
        if (loading) {
            this.showLoading();
            let content = this.filters.content;
            content = (content === 'artwork') ? content : content+'s';
            let tax = '';
            let taxonomies = this.getCurrentTaxonomies();
            if(!isEmptyObject(taxonomies)){
                let total = 0;
                total = Object.values(taxonomies).map((tax)=> total+tax.length);
                let all = [];
                let join = (total[0] === 2) ? ' and ' : ', ';
                tax = Object.values(taxonomies).map((tax) => all.push(tax.join(join)));
                tax = all.join(', ');
                let index = tax.lastIndexOf(',')+1;
                if(index> 0){
                    tax = tax.substr(0, index)+' and'+tax.substr(index);
                }
            }
            this.a11y.announce(`Checking for more ${tax} ${content}...`);
        } else {
            this.hideLoading();
        }
        // Update loading spinner
        this.elements.spinner.hidden = !loading;
        // Update load more button
        this.elements.loadMore.disabled = loading;
    }
    /**
     * Show the loading overlay
     */
    showLoading() {
        this.hideBody();
        this.elements.loading.classList.add('active');
        this.startQuipCycle();
        document.body.classList.add('loading');
    }
    /**
     * Hide the loading overlay
     */
    hideLoading() {
        this.showBody();
        this.container.classList.remove('active');
        this.stopQuipCycle();
        document.body.classList.remove('loading');
    }
    /**
     * Start cycling through loading messages
     */
    startQuipCycle() {
        if (this.quipInterval) {
            clearInterval(this.quipInterval);
        }
        if (!this.quips.length) return;
        // Set initial message
        this.updateMessage(this.quips[0]);
        this.container.classList.remove('changing');
        this.quipInterval = setInterval(() => {
            this.container.classList.add('changing');
            setTimeout(() => {
                this.loadingIndex = (this.loadingIndex + 1) % this.quips.length;
                this.updateMessage(this.quips[this.loadingIndex]);
                setTimeout(() => {
                    this.container.classList.remove('changing');
                }, 50);
            }, 350);
        }, this.loadingOptions.cycleInterval);
    }
    /**
     * Stop cycling through loading messages
     */
    stopQuipCycle() {
        if (this.quipInterval) {
            clearInterval(this.quipInterval);
            this.quipInterval = null;
        }
    }
    /**
     * Update the loading message
     */
    updateMessage(quipData) {
        if (!this.loadingMessage) return;
        const icon = feedSettings?.icons?.[quipData.icon] || '';
        this.loadingMessage.innerHTML = `${icon}<p>${quipData.quip}</p>`;
    }
    /**
     * Set a custom loading message
     */
    setMessage(message, icon = null) {
        if (!this.loadingMessage) return;
        this.stopQuipCycle();
        const iconHtml = icon ? feedSettings?.icons?.[icon] || '' : '';
        this.loadingMessage.innerHTML = `${iconHtml}<p>${message}</p>`;
    }
    /**
     * Shuffle an array (Fisher-Yates algorithm)
     */
    shuffleArray(array) {
        const newArray = [...array];
        for (let i = newArray.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
        }
        return newArray;
    }
    /**
     * Initialize quips for loading messages
     */
    initializeQuips() {
        // Map pstyle, artstyle, etc. to their base types for quips
        const typeMapping = {
            'pstyle': 'style',
            'artstyle': 'style',
            'arttheme': 'theme',
            'artmedium': 'style',
            'artform': 'style',
            'placement': 'style',
            'colour': 'style'
        };
        // Gather all quips based on content types and taxonomies
        const allQuips = [];
        // Add content type quips
        if (FeedBlock.LOADING_QUIPS[this.filters.content]) {
            FeedBlock.LOADING_QUIPS[this.filters.content].forEach(quip => {
                allQuips.push({
                    icon: this.filters.content,
                    quip: quip
                });
            });
        }
        // Add taxonomy quips
        if(this.filters.taxonomies && !isEmptyObject(this.filters.taxonomies)){
            Object.keys(this.filters.taxonomies).forEach(taxonomy => {
                const baseType = typeMapping[taxonomy] || taxonomy;
                if (FeedBlock.LOADING_QUIPS[baseType]) {
                    FeedBlock.LOADING_QUIPS[baseType].forEach(quip => {
                        allQuips.push({
                            icon: taxonomy,
                            quip: quip
                        });
                    });
                }
            });
        }
        // Shuffle the quips array
        return this.shuffleArray(allQuips);
    }
    /**
     * Feed Grid
     */
    renderItems(items, append = false) {
        // Reset if not appending
        if (!append) {
            window.removeChildren(this.elements.grid);
            this.feed.loaded = 0;
            this.feed.gallery = [];
        }
        // Add items to gallery if in gallery mode
        if (this.config.isGallery) {
            this.feed.gallery = this.feed.gallery.concat(items);
        }
        // Bail early if no items
        if (items.length === 0) {
            this.a11y.announceUpdate(0, append);
            return;
        }
        // Use DocumentFragment for better performance
        const fragment = document.createDocumentFragment();
        // Process items in batches for better performance
        const batchSize = 10;
        const processBatch = (startIndex) => {
            const endIndex = Math.min(startIndex + batchSize, items.length);
            // Process this batch
            for (let i = startIndex; i < endIndex; i++) {
                const item = items[i];
                const element = this.createItemElement(item);
                fragment.appendChild(element);
                // Lazy load images beyond threshold
                if (this.feed.loaded >= this.feed.imageLoadThreshold && this.imageObserver) {
                    this.imageObserver.observe(element);
                } else {
                    this.loadImage(element);
                }
                this.feed.loaded++;
            }
            // If we have more items, process next batch in next frame
            if (endIndex < items.length) {
                requestAnimationFrame(() => {
                    processBatch(endIndex);
                });
            } else {
                // All batches processed, append fragment
                this.elements.grid.appendChild(fragment);
                if(this.container.dataset.gallery){
               console.log(this.getGalleryItems());
                    this.updateGalleryItems(this.getGalleryItems());
                    //pagination already updated, so it'll be page 2
                    if(this.getHighlight() && !this.highlightGot){
                        this.highlightGot = true;
                        this.openGallery(0);
                    }
                }
                this.a11y.makeNavigable(this.elements.grid.querySelectorAll('.feed-item:not([data-keyboard-nav])'));
                this.a11y.announceItems(items.length, append, this.state.hasMore);
            }
        };
        // Start processing the first batch
        if (items.length > 0) {
            processBatch(0);
        } else {
            this.a11y.announceUpdate(0, append);
        }
    }
    /**
     * Load image for an item
     */
    loadImage(element) {
        const img = element.querySelector('img');
        if (!img) return;
        const size = this.getImageSize();
        img.src = img.dataset[size] || img.dataset.src;
        delete img.dataset.src;
        element.setAttribute('data-loaded', 'true');
    }
    /**
     * Update image sizes based on screen width
     */
    updateImageSizes() {
        const size = this.getImageSize();
        // Update only visible images that aren't already loaded with the right size
        const items = this.elements.grid.querySelectorAll('.feed-item[data-loaded="true"]');
        items.forEach(item => {
            const img = item.querySelector('img');
            if (img && img.dataset[size] && img.src !== img.dataset[size]) {
                img.src = img.dataset[size];
            }
        });
    }
    /**
     * Create an element for a feed 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);
        }
        const favourited = window.isFavourited(item.icon, item.id)??false;
        const template = window.getTemplate('feed-item');
        // Set unique attributes
        template.id = `${item.icon}-${item.id}`;
        template.classList.add(item.icon);
      if (item.umami_view) {
         this.buildUmamiData(template, item.umami_view);
      }
      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);
            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);
                  image.href = c.url;
                  let itemImg = image.querySelector('img');
                  itemImg.src = c.image.small;
                  itemImg.alt = c.image.alt;
                  images.append(image);
               });
            }
            img.remove();
         }
      }
      single.remove();
      list.remove();
        this.rendered[item.icon].set(item.id, template);
        return template;
    }
    buildImageData(img, data){
      if (typeof data.tiny !== 'string') {
class FeedBlockOld {
   constructor() {
      this.container = document.querySelector('section.feed-block');
      if (!this.container) {
         return;
      }
        [
            img.src,
            img.dataset.small,
            img.dataset.medium,
            img.dataset.large,
            img.alt
        ] =
        [
            data.tiny,
            data.small,
            data.medium,
            data.large,
            data.alt
        ];
    }
    buildUmamiData(item, data){
        for(let [key, value] of Object.entries(data)){
            item.dataset[key] = value;
        }
    }
      this.a11y = window.jvbA11y;
      this.cache = new window.jvbCache('feed');
      this.error = window.jvbError;
    /**
     * Show empty state
     */
    showEmptyState() {
        const message = this.filters.favourites
            ? `<div class="feed-empty-state">
                <h3>♡ BLANK CANVAS â™¡</h3>
                <p>You haven't fallen in love with any pieces... yet!</p>
                <p>Hit that heart icon when something stops your scroll.</p>
                <p>Your dream collection is waiting to start.</p>
            </div>`
            : `<div class="feed-empty-state">
                <h3>NOTHING HERE...</h3>
                <p>Try tweaking those filters.</p>
                <p>Edmonton's got talent - let's find it.</p>
            </div>`;
      this.config = {
         source: '',
         context: '',
         highlight: null,
         gallery: false,
         view: this.cache.get('feedView') || 'grid',
         ... this.container.dataset
      };
      this.initElements();
      this.initFilters();
        this.elements.grid.innerHTML = message;
        this.a11y.announceEmpty(this.filters.favourites);
    }
    /**
     * Clear the grid
     */
    clearGrid() {
        this.a11y.announce('Items cleared.');
        window.removeChildren(this.elements.grid);
        this.feed.loaded = 0;
    }
      this.loadWhenAble();
   }
    /**
     * Get image size based on screen width
     */
    getImageSize() {
        const width = window.innerWidth;
        if (width > 1024) return 'medium';
        if (width > 500) return 'medium';
        return 'small';
    }
   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);
      }
   }
    /**
     * Get gallery items for the gallery modal
     */
    getGalleryItems() {
        return Array.from(this.container.querySelectorAll('.feed-item'))
            .map(item => {
                const img = item.querySelector('img');
                if (!img) return null;
   initElements() {
      this.currentTaxonomies = new Set();       // Allowed Taxonomies, grabbed from active buttons
      this.taxonomyFilters = {};
      this.elements = {
         filterTrigger: '[data-filter]',
         filters: {
            content: '[data-filter="content"]',
            orderby: '[data-filter="orderby"]',
            order: '[data-filter="order"]',
            match: '[data-filter="match"]',
            favourites: '[data-filter="favourites"]',
            taxonomy: '[data-filter^="taxonomy"]'
         },
         selectedTax: '.selected-items',
         clearFilter: 'button.clear-filters',
         loadMore: 'button.load-more',
         filterContainer: '.filters',
         grid: '.item-grid',
      };
      this.ui = window.uiFromSelectors(this.elements);
                return {
                    id: item.querySelector('button.favourite').dataset.id,
                    small: img.dataset.small || img.src,
                    large: img.dataset.medium || img.src,
                    full: img.dataset.full || img.src,
                    alt: img.alt || '',
                    fav: item.querySelector('button.favourite')?.cloneNode(true),
                    info: item.querySelector('.item-info')?.cloneNode(true)
                };
            })
            .filter(Boolean);
    }
    /**
     * Clean up resources when component is destroyed
     */
    addEvent(element, event, handler, options) {
        if (!element) return;
      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']];
      }
        const boundHandler = handler.bind(this);
        element.addEventListener(event, boundHandler, options);
      if (this.ui.taxonomies.length>0) {
         this.taxonomies = Array.from(
            this.ui.taxonomies,
         ).map(content => content.dataset.taxonomy);
      } else {
         this.taxonomies = [];
      }
        // Store for cleanup
        if (!this.eventHandlers.has(element)) {
            this.eventHandlers.set(element, []);
        }
        this.eventHandlers.get(element).push({ event, handler: boundHandler });
    }
    destroy() {
        // Clean up observers
        if (this.imageObserver) {
            this.imageObserver.disconnect();
            this.imageObserver = null;
        }
        if (this.resizeObserver) {
            this.resizeObserver.disconnect();
            this.resizeObserver = null;
        }
   }
        // Clean up all event listeners
        this.eventHandlers.forEach((handlers, element) => {
            handlers.forEach(({ event, handler }) => {
                element.removeEventListener(event, handler);
            });
        });
        this.eventHandlers.clear();
   async initTaxonomies() {
      this.selector = window.jvbSelector;
      const buttons = document.querySelectorAll('[data-filter="taxonomy"]');
        // Clean up timers
        if (this.quipInterval) {
            clearInterval(this.quipInterval);
        }
      this.selector.isInitializing = true;
      buttons.forEach((button) => {
         const taxonomy = button.dataset.taxonomy;
         this.currentTaxonomies.add(taxonomy);
        if (this.timeoutId) {
            clearTimeout(this.timeoutId);
        }
         this.selector.registerFilterButton(button, {
            button: button,
            buttonSelector: '[data-filter="taxonomy"]',
            selected: this.ui.selectedTax
         });
        // Clear template cache and other state
        this.feed.templates.clear();
        this.feed.gallery = [];
        this.feed.loaded = 0;
    }
    /** Extra Term Handling **/
         // Add preload listeners
         this.addTaxonomyPreloadListeners(button, taxonomy);
      });
    getSelectedTaxonomies(){
        let taxonomies = {};
        for(var [taxonomy, instance] of Object.entries(this.selectorInstances)){
            taxonomies[taxonomy] = instance.selectedItems;
        }
        return taxonomies;
    }
    /**
     * Get selected values for a taxonomy
     */
    getSelectedTerms(taxonomy) {
        const selectedItems = this.elements.selected.querySelectorAll(
            `.selected-item[data-taxonomy="${taxonomy}"]`
        );
      this.selector.isInitializing = false;
        return Array.from(selectedItems).map(item => item.dataset.id);
    }
      this.selector.subscribe((event, data) => {
         if (event === 'selected-terms') this.handleTaxonomyChange(data);
      });
   }
    clearSelectedTaxonomies(){
        window.removeChildren(this.elements.selected);
        if(!isEmptyObject(this.selectorInstances)){
            for(var [taxonomy, instance] of Object.entries(this.selectorInstances)){
                instance.selectedItems = {};
            }
        }
   addTaxonomyPreloadListeners(button, taxonomy) {
      const preload = () => {
         this.selector.preloadTaxonomy(taxonomy);
      };
        this.elements.matchAll.querySelector(input).checked = false;
      // Desktop hover
      button.addEventListener('mouseenter', preload, { once: true });
        this.filters.taxonomies = {};
        this.updateFilters();
    }
    setSelectedTerms(taxonomy){
        if(this.selectorInstances[taxonomy]){
            let selected = this.selectorInstances[taxonomy].selectedItems;
            if(!isEmptyObject(selected)){
      // Touch/keyboard (fires before click)
      button.addEventListener('pointerdown', preload, { once: true });
                this.filters.taxonomies[taxonomy] = selected;
                for(var [id, name] of Object.entries(selected)){
                    this.elements.selected.appendChild(this.createFilterTag(taxonomy, id, name));
                }
                this.updateFilters();
                this.updateSelectedListeners();
            }else{
                delete this.filters.taxonomies[taxonomy];
            }
      // Keyboard focus
      button.addEventListener('focus', preload, { once: true });
   }
        }
    }
    clearSelectedTerm(termId, taxonomy){
        let container = this.container.querySelector('.filters');
        let input = container.querySelector(`li[data-id="${termId}"] input`);
        input.checked = false
        delete this.selectorInstances[taxonomy].selectedItems[termId];
    }
    clearSelectedTerms(taxonomy){
   handleTaxonomyChange(data) {
      const { terms, taxonomy } = data;
        if(!isEmptyObject(this.filters.taxonomies) && this.filters.taxonomies.hasOwnProperty(taxonomy)){
            delete this.filters.taxonomies[taxonomy];
        }
        if(!isEmptyObject(this.selectorInstances) && this.selectorInstances.hasOwnProperty(taxonomy)){
            this.selectorInstances[taxonomy].selectedItems = {};
        }
      // 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
      };
        const selectedItems = this.elements.selected.querySelectorAll(
            `.selected-item[data-taxonomy="${taxonomy}"]`
        );
      // Add taxonomy filters if any exist
      if (Object.keys(this.taxonomyFilters).length > 0) {
         filters.taxonomy = this.taxonomyFilters;
      }
        if(selectedItems.length > 0){
            selectedItems.forEach(item => {
                item.remove();
            });
        }
      this.updateFilter(filters);
   }
        // Update clear filters button visibility
        this.updateClearFiltersButton();
    }
    updateClearFiltersButton(){
        if (!this.elements.clearFilters) return;
   clearAllTaxonomies() {
      this.taxonomyFilters = {};
      window.removeChildren(this.ui.selectedTax);
        let filters = this.elements.selected.children.length;
      this.updateFilter({
         taxonomy: null,
         page: 1
      });
   }
        const hasFilters = filters > 0;
   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;
        const hasMultiple = filters > 1;
      //check the cache
      this.processCachedFilters();
      //check url
      this.processURLFilters();
        this.elements.clearFilters.hidden = !hasFilters;
      // 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;
            }
         });
      }
        this.elements.filters.classList.toggle('has-filters', hasFilters);
      // Update content-specific visibility
      this.updateContentFor(this.filters.content);
   }
   nextPage() {
      this.store.setFilter('page', this.store.filters.page++);
   }
        this.elements.matchAll.hidden = !hasMultiple;
    }
   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;
         }
      });
   }
    /**
     * Create a filter tag element
     */
    createFilterTag(taxonomy, id, name) {
        const tag = window.getTemplate('selectedTerm');
   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();
               }
            }
         });
      }
   }
        tag.dataset.taxonomy = taxonomy;
        tag.dataset.id = id;
        let icon = window.getIcon(taxonomy);
        let span = tag.querySelector('span');
        let button = tag.querySelector('button');
        tag.prepend(icon);
        span.classList.add('filter-name');
        span.classList.remove('item-name');
        [span.textContent, button.remove] =
            [escapeHtml(name), `Remove ${escapeHtml(name)}`];
   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;
            }
         }
      });
        return tag;
    }
      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;
   }
    /**
     * Gallery
     **/
    openGallery(index){
        this.gallery.index = index;
        this.gallery.modal.showModal();
        this.hideBody();
   /**
    * Update URL with current filters (for sharing/bookmarking)
    */
   updateURL() {
      const params = new URLSearchParams();
        this.bindGalleryEvents();
        //show current image
        this.updateDisplay(index);
        //preload adjacent images
        this.preloadImages();
      // Add simple filters
      ['content', 'order', 'orderby', 'match'].forEach(key => {
         if (this.filters[key]) {
            params.set(`f_${key}`, this.filters[key]);
         }
      });
        // Announce initial state
        this.a11y.announce(`Image ${this.gallery.index + 1} of ${this.gallery.items.length}. Use arrow keys to navigate.`)
        this.a11y.trapFocus(this.gallery.modal);
      // 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);
   }
    /**
     * Create the modal element
     */
    createGalleryModal() {
        const modal = document.createElement('dialog');
        modal.className = 'gallery-modal';
        modal.setAttribute('aria-modal', 'true');
        modal.setAttribute('aria-label', 'Image Gallery');
   renderItems() {
      let items = this.store.getFiltered();
      if (this.store.filters['page'] === 1) {
         window.removeChildren(this.ui.grid);
      }
        modal.innerHTML = `
        <button class="gallery-close" aria-label="Close gallery">
          ${jvbSettings.icons.close}
        </button>
      if (items.length === 0) {
         this.a11y.announceItems(0, this.store.filters['page'] > 0);
         return;
      }
        <button class="gallery-nav gallery-prev" aria-label="Previous image">
          ${jvbSettings.icons.prev}
        </button>
      const fragment = document.createDocumentFragment();
      const batchSize = 10;
        <button class="gallery-nav gallery-next" aria-label="Next image">
             ${jvbSettings.icons.next}
        </button>
      const processBatch = (startIndex) => {
         const endIndex = Math.min(startIndex + batchSize, items.length);
        <div class="gallery-content">
          <img src="" alt="" class="gallery-image">
          <details>
            <summary>DETAILS</summary>
            <div class="item-info"></div>
          </details>
        </div>
         for (let i = startIndex; i < endIndex; i++) {
            const item = items[i];
            const element = this.createItemElement(item);
            fragment.appendChild(element);
         }
        <div class="gallery-favourite"></div>
        <div class="gallery-counter"><span id="gallery-index">1</span> / <span class="total"></span></div>
    `;
         if (endIndex < items.length) {
            requestAnimationFrame(() => processBatch(endIndex));
         } else {
            this.removePlaceholders();
            this.ui.grid.append(fragment);
        return modal;
    }
            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);
         }
      };
    /**
     * Bind event handlers
     */
    bindGalleryEvents() {
        // Close button
        this.gallery.modal.querySelector('.gallery-close').addEventListener('click', () => this.closeGallery());
      if (items.length > 0) {
         processBatch(0);
      } else {
         this.a11y.announceItems(0, this.store.filters['page'] >1, false);
      }
        // Navigation buttons
        const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
        const nextBtn = this.gallery.modal.querySelector('.gallery-next');
      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;
      }
   }
        prevBtn.addEventListener('click', () => this.navigate(-1));
        nextBtn.addEventListener('click', () => this.navigate(1));
   /**
    *
    * @param {object} item
    */
   createItemElement(item) {
      let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
        // Keyboard navigation
        this.gallery.keyHandler = (e) => {
            switch (e.key) {
                case 'ArrowLeft':
                    this.navigate(-1);
                    break;
                case 'ArrowRight':
                    this.navigate(1);
                    break;
                case 'Escape':
                    this.closeGallery();
                    break;
            }
        };
        document.addEventListener('keydown', this.gallery.keyHandler);
      const isTimeline = Object.hasOwn(template.dataset, 'timeline');
        // Touch events
        this.gallery.modal.addEventListener('touchstart', (e) => {
            this.gallery.touchStart = e.touches[0].clientX;
        });
      // 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;
        this.gallery.modal.addEventListener('touchmove', (e) => {
            this.gallery.touchEnd = e.touches[0].clientX;
        });
         if (value === '') {
            el.remove();
            continue;
         }
        this.gallery.modal.addEventListener('touchend', () => {
            if (!this.gallery.touchStart || !this.gallery.touchEnd) return;
         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);
         }
      }
            const distance = this.gallery.touchStart - this.gallery.touchEnd;
            const isLeftSwipe = distance > this.gallery.minSwipe;
            const isRightSwipe = distance < -this.gallery.minSwipe;
      // Handle link
      let link = template.querySelector('a');
      if (link && item.url !== '') {
         [
            link.href,
            link.title
         ] = [
            item.url,
            `View ${item.fields['post_title']??'Item'}`
         ];
      }
            if (isLeftSwipe) {
                this.navigate(1);
            } else if (isRightSwipe) {
                this.navigate(-1);
            }
      if (isTimeline) {
         this.addTimelineElements(item, template);
      }
            this.gallery.touchStart = null;
            this.gallery.touchEnd = null;
        });
    }
      return template;
   }
   splitIDs(value) {
      return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
   }
   isImageField(item, value) {
      if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) {
         return false;
      }
      let values = this.splitIDs(value);
    /**
     * Navigate to previous/next image
     */
    async navigate(direction) {
        const newIndex = this.gallery.index + direction;
      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;
        // Check if out of bounds
        if (newIndex < 0 || newIndex >= this.gallery.items.length) {
            this.a11y.announceNavigation(newIndex, this.gallery.items.length,direction < 0, direction > 0)
            return;
        }
      if (values.length > 1) {
         let image = element.querySelector('img');
         if (!image) return;
         values.forEach(imgID => {
            let img = image.cloneNode(true);
            this.formatImageField(img, imgID, item);
            element.append(img);
         });
         image.remove();
      } else {
         if (element.tagName !== 'IMG') {
            element = element.querySelector('img');
            if (!element) return;
         }
         this.formatImageField(element, values[0], item);
      }
   }
   formatImageField(element, value, item) {
      let imgData = item.images[value]??false;
      if (!imgData) return;
      [
         element.src,
         element.srcset,
         element.alt
      ] = [
         imgData.tiny,
         `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`,
         imgData['image-alt-text']
      ]
   }
   isTaxonomyField(item, field) {
      if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) {
         return false;
      }
        // Update current index
        this.gallery.index = newIndex;
      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;
        // Update display
        this.updateDisplay(newIndex);
         [
            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;
   }
        // Preload adjacent images
        this.preloadImages();
   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')
      ];
        // Announce to screen readers
        this.a11y.announceNavigation(this.gallery.index,this.gallery.items.length);
      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 more if we're near the end
        if (direction > 0 && newIndex >= (this.gallery.items.length - 3) && this.state.hasMore) {
            await this.fetchFeed();
            this.updateGalleryItems(this.getGalleryItems());
        }
    }
   removePlaceholders() {
      const placeholders = this.ui.grid.querySelectorAll('.placeholder');
      if (placeholders.length > 0) {
         placeholders.forEach(p => p.remove());
      }
   }
    /**
     * Preload adjacent images
     */
    preloadImages() {
        // Preload current, previous and next images
        [-1, 0, 1].forEach(offset => {
            const index = this.gallery.index + offset;
            if (index >= 0 && index < this.gallery.items.length) {
                const img = new Image();
                const item = this.gallery.items[index];
                if (window.innerWidth < 1000) {
                    img.src = item.large || item.src;
                } else {
                    img.src = item.full || item.src;
                }
            }
        });
    }
   addPlaceholders() {
      let total = this.contentTypes.length;
      const fragment = document.createDocumentFragment();
      for (let i = 0; i < 12; i++) {
         let template = window.getTemplate('placeholderTemplate');
    /**
     * Update display with current image
     */
    updateDisplay(index) {
        const item = this.gallery.items[index];
        if (!item) return;
         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);
   }
        // Get elements
        const favourite = this.gallery.modal.querySelector('.gallery-favourite');
        const image = this.gallery.modal.querySelector('.gallery-image');
        const counter = this.gallery.modal.querySelector('.gallery-counter');
        const info = this.gallery.modal.querySelector('.item-info');
        // Update image
        image.src = window.innerWidth < 1000 ?
            (item.large || item.src) :
            (item.full || item.src);
        image.alt = item.alt || '';
   /**
    *
    * @param {object} filters {name: value}
    */
   updateFilter(filters) {
      //double check filters are what we're expecting
      let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)];
        // Update favourite button
        if (favourite && item.fav) {
            window.removeChildren(favourite);
            favourite.appendChild(item.fav.cloneNode(true));
        }
      filters = Object.keys(filters)
         .filter(key => allowed.includes(key))
         .reduce((obj, key) => {
            obj[key] = filters[key];
            return obj;
         }, {});
        // Update info
        if (info && item.info) {
            window.removeChildren(info);
            const clone = item.info.cloneNode(true);
            info.appendChild(clone);
        }
      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 counter
        counter.textContent = `${this.gallery.index + 1} / ${this.gallery.items.length}`;
      // Update ordering options
      const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]');
      orderButtons.forEach(button => {
         const forTypes = button.dataset.for?.split(',') || [];
         if (forTypes.length > 0) {
            button.hidden = !forTypes.includes(contentType);
            // Uncheck if hiding
            if (button.hidden && button.checked) {
               button.checked = false;
            }
         }
      });
        // Update navigation buttons
        this.updateNavigationButtons();
    }
      // Update order direction visibility based on selected orderby
      const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked');
      this.updateOrderDirectionVisibility(orderBy?.value);
   }
    /**
     * Update navigation button visibility
     */
    updateNavigationButtons() {
        const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
        const nextBtn = this.gallery.modal.querySelector('.gallery-next');
   /**
    * 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
         });
      }
        prevBtn.classList.toggle('end', this.gallery.index > 0 ? '' : 'none');
        nextBtn.classList.toggle('end', this.gallery.index < this.gallery.items.length - 1 ? '' : 'none');
    }
      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
            );
         });
      }
    /**
     * Close the gallery
     */
    closeGallery() {
        this.showBody();
        // Remove event listeners
        document.removeEventListener('keydown', this.gallery.keyHandler);
        this.a11y.announce('Gallery closed.');
      window.addEventListener('popstate', this.popStateHandler);
      document.addEventListener('click', this.clickHandler);
      document.addEventListener('change', this.changeHandler);
   }
        this.gallery.modal.close();
   handlePopState(e) {
      if (e.state?.filters) {
         if (this.processURLFilters()) {
            this.store.setFilters(this.filters);
            this.a11y.announce('Feed filters updated from browser history');
         }
      }
   }
        // Reset state
        this.gallery.keyHandler = null;
    }
   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);
      }
   }
    /**
     * Update gallery items
     * @param {Array} newItems - New gallery items
     */
    updateGalleryItems(newItems) {
        // Store original current index and item
        const currentItem = this.gallery.items[this.gallery.index];
   handleRemoveSelectedTerm(e) {
      const selectedItem = e.target.closest('.selected-item');
      if (!selectedItem) return;
        // Update items array
        this.gallery.items = newItems;
      const termId = parseInt(selectedItem.dataset.id);
      const taxonomy = selectedItem.dataset.taxonomy;
        // Try to keep the same item selected
        if (currentItem) {
            // Find the same item in the new array by matching source
            const newIndex = this.gallery.items.findIndex(item =>
                item.full === currentItem.full ||
                item.large === currentItem.large
            );
      // Remove from filters
      if (this.taxonomyFilters[taxonomy]) {
         this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy]
            .filter(id => id !== termId);
            if (newIndex !== -1) {
                this.gallery.index = newIndex;
            }
        }
         if (this.taxonomyFilters[taxonomy].length === 0) {
            delete this.taxonomyFilters[taxonomy];
         }
      }
        // Update navigation buttons
        this.updateNavigationButtons();
    }
      // Remove from UI
      selectedItem.remove();
    /**
     * Ensure gallery is accessible
     */
    setupGalleryAccessibility() {
        // Add ARIA attributes
        this.gallery.modal.setAttribute('aria-modal', 'true');
        this.gallery.modal.setAttribute('aria-label', 'Image Gallery');
    }
      // Update filters
      this.updateFilter({
         taxonomy: Object.keys(this.taxonomyFilters).length > 0
            ? this.taxonomyFilters
            : null,
         page: 1
      });
   }
    hideBody(){
        document.body.style.overflow = 'hidden';
    }
    showBody(){
        document.body.style.overflow = '';
    }
   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 });
         }
      }
   }
}
// Initialize feed blocks when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
    document.querySelectorAll('.feed-block').forEach(container => {
        // Initialize with both the container and overlay
        window.feedBlock = new FeedBlock(container);
    });
document.addEventListener('DOMContentLoaded', async function() {
   window.auth.subscribe(event => {
      if (event === 'auth-loaded') {
         window.feedBlock = new FeedBlock();
      }
   });
});
function isEmptyObject(obj) {
    return Object.keys(obj).length === 0;
}