Jake Vanderwerf
6 hours ago 56a9a1ccf764ff7a6af8f8a2292cb07443cb4aa7
assets/js/concise/TaxonomySelector.js
@@ -1,1207 +1,1382 @@
/**
 * Centralized Taxonomy Selector with DataStore Integration
 * Handles all taxonomy selection fields using DataStore for state management
 */
class TaxonomySelector {
   constructor() {
      this.container = document.querySelector('dialog#jvb-selector');
      if (!this.container) return;
      this.a11y = window.jvbA11y;
      this.error = window.jvbError;
      this.index = -1;
      // DataStore instances per taxonomy
      this.stores = new Map();
      this.storeSubscriptions = new Map();
      // Central field management
      this.subscribers = new Set();
      this.fields = new Map();
      this.selectedTerms = new Map();    // Current modal selection
      this.selectedTerms = new Map();  // a map of fieldId => Set of selected term Ids
      this.batchFetch = new Set();
      // Current modal context
      this.activeField = null;
      this.currentConfig = null;
      this.currentSingular = null;
      this.currentPlural = null;
      this.activeStore = null;
      // Modal state
      this.disabled = false;
      // Search debouncing
      this.searchHandler = null;
      this.isInitializing = true;
      this.lazyInit = false;
      this.messageText = {}
      this.init();
   }
   /**
    * Initialize the selector
    */
   init() {
      this.initStore();
      this.initElements();
      this.defineTemplates();
      this.initModal();
      this.scanExistingFields();
      this.initGlobalListeners();
      this.initListeners();
      if (this.needsCreator() && window.jvbTaxCreator) {
         this.creator = new window.jvbTaxCreator(this);
      }
      this.isInitializing = false
      this.batchFetchTaxonomies().then(()=> {});
   }
   /**
    * Get or create a DataStore for a taxonomy
    */
   getOrCreateStore(taxonomy) {
      if (!this.stores.has(taxonomy)) {
         const store = new window.jvbStore({
            name: `tax_${taxonomy}`,
   initStore() {
      const store = window.jvbStore.register(
         'taxonomies',
         {
            storeName: 'terms',
            keyPath: 'id',
            showLoading: false,
            indexes: [
               {name: 'taxonomy', keyPath: 'taxonomy'},
               {name: 'parent', keyPath: 'parent'},
               {name: 'slug', keyPath: 'slug'},
               {name: 'count', keyPath: 'count'},
            ],
            endpoint: 'terms',
            TTL: 3600000, // 1 hour cache
            TTL: 2 * 60 * 1000,
            filters: {
               taxonomy: taxonomy,
               taxonomy: '',
               page: 1,
               search: '',
               parent: 0
            },
            required: 'taxonomy',
            delayFetch: true,
         }
      );
      this.store = store.terms;
      this.store.subscribe(this.handleStoreEvent.bind(this));
   }
   defineTemplates() {
      const T = window.jvbTemplates;
      const terms = this;
      T.define('emptyState');
      T.define('selectedTerm', {
         refs: {
            name: '.item-name',
            btn: 'button',
         },
         setup({el, refs, manyRefs, data}) {
            el.dataset.id = data.id;
            el.dataset.taxonomy = data.taxonomy;
            if (refs.name) refs.name.textContent = data.path;
            if (refs.button) refs.button.title = `Remove ${data.name}`;
         }
      });
      T.define('termListItem', {
         refs: {
            checkbox: 'input',
            label: 'label',
            name: 'span, .term-name'
         },
         setup({el, refs, manyRefs, data}) {
            el.dataset.id = data.id;
            let field = terms.currentField();
            let isSelected = terms.selectedTerms.get(terms.activeField).has(data.id);
            let limitReached = field.limit > 0 && terms.selectedTerms.get(terms.activeField).size >= field.limit;
            if (refs.checkbox) {
               refs.checkbox.dataset.id = data.id;
               refs.checkbox.id = `${field.id}-${data.id}`;
               refs.checkbox.name = `${field.id}-${field.taxonomy}-select`;
               refs.checkbox.value = data.id;
               refs.checkbox.disabled = !isSelected && limitReached;
               refs.checkbox.checked = isSelected;
            }
            if (refs.label) {
               refs.label.htmlFor = `${field.id}-${data.id}`;
               refs.label.title = data.path??data.name;
               refs.label.dataset.path = data.path;
            }
            if (refs.name) {
               refs.name.textContent = data.show ? data.path : data.name;
            }
            if (data.hasChildren) {
               let temp = {
                  plural: field.plural,
                  name: data.name
               };
               const toggle = window.jvbTemplates.create('termChildrenToggle', temp);
               el.append(toggle);
            }
         }
      });
      T.define('termChildrenToggle', {
         setup({el, refs, manyRefs, data}) {
            el.ariaLabel = `View ${data.plural} nested under ${data.name}`;
         }
      });
      T.define('termBreadcrumb', {
         setup({el, refs, manyRefs, data}) {
            el.dataset.id = data.id;
            el.textContent = data.name;
            el.title = data.name;
         }
      });
      T.define('autocompleteItem', {
         setup({el, refs, manyRefs, data}) {
            el.dataset.id = data.id;
            el.textContent = data.path||data.name;
            el.title = `Select ${data.name}`;
         }
      });
   }
   /******************************************************************
    ELEMENTS
    ******************************************************************/
   initElements() {
      this.selectors = {
         search: {
            input: '[type="search"]',
            clear: '.clear-search',
            container: '.search-wrapper',
            results: '.search-results',
         },
         create: {
            button: 'button.submit-term',
            span: '.submit-term span',
         },
         terms: {
            list: '.items-container',
            wrap: '.items-wrap',
            sentinel: '.scroll-sentinel',
         },
         nav: {
            nav: 'nav.term-navigation',
            back: '.back-to-parent',
            child: '.toggle-children',
            pathLevel: '.path-level',
         },
         message: {
            message: 'p.message',
            text: 'p.message span',
         },
         selected: '.selected-items',
         modal: {
            title: '#modal-title',
            content: '.modal-content',
            count: '.selection-count'
         },
         favourites: '.favourite-terms',
         field: {
            toggle: 'button.selector-toggle, [data-filter="taxonomy"]',
            value: 'input[type="hidden"]',
            selected: '.selected-items',
            dropdown: {
               list: '.search-results',
               wrapper: '.auto-wrapper',
            },
            create: {
               button: '.auto-wrapper .submit-term',
               span: '.auto-wrapper button span',
            },
            search: 'input[data-autocomplete]',
            message: {
               message: 'p.message',
               text: 'p.message span',
            },
         }
      }
      this.ui = window.uiFromSelectors(this.selectors, this.container);
   }
   initListeners() {
      this.observer = new IntersectionObserver((entries) => {
         entries.forEach(entry => {
            if (entry.isIntersecting) {
               this.nextPage();
            }
         });
         // Subscribe to store events
         const unsubscribe = store.subscribe((event, data) => {
            this.handleStoreEvent(taxonomy, event, data);
         });
         this.stores.set(taxonomy, store);
         this.storeSubscriptions.set(taxonomy, unsubscribe);
      }
      return this.stores.get(taxonomy);
   }
   /**
    * Handle DataStore events
    */
   handleStoreEvent(taxonomy, event, data) {
      // Only process events for the active taxonomy in modal
      if (this.activeStore && this.activeStore.config.name === `tax_${taxonomy}`) {
         switch (event) {
            case 'items-loaded':
            case 'data-fetched':
            case 'data-cached':
            case 'stale-cache-used':
               this.handleTermsLoaded(data);
               break;
            case 'fetch-error':
               this.handleFetchError(data.error);
               break;
            case 'filters-changed':
               // Could trigger UI updates for active filters
               break;
         }
      }
      // Handle field-specific updates outside modal
      if (event === 'items-updated' || event === 'items-loaded') {
         this.updateFieldsForTaxonomy(taxonomy, data.items);
      }
   }
   /**
    * Handle loaded terms from DataStore
    */
   handleTermsLoaded(data) {
      this.hideLoading();
      const terms = data.data?.items || [];
      const pagination = data.data?.pagination || {};
      const isSearch = data.filters?.search && data.filters.search.length > 0;
      const append = data.filters?.page > 1;
      if (terms.length === 0) {
         if (!append) {
            this.showEmptyState(isSearch ? 'No results found.' : 'No items available.');
         }
         this.observer.unobserve(this.ui.sentinel);
      } else {
         this.renderTerms(terms, append, isSearch);
         this.currentTerms = terms;
         // Handle pagination
         if (pagination.has_more) {
            this.observer.observe(this.ui.sentinel);
         } else {
            this.observer.unobserve(this.ui.sentinel);
         }
      }
      // Announce to screen readers
      this.a11y?.announce(terms.length, append);
   }
   /**
    * Handle fetch errors
    */
   handleFetchError(error) {
      console.error('Taxonomy fetch error:', error);
      this.hideLoading();
      if (this.error?.log) {
         this.error.log(error, {
            component: 'TaxonomySelector',
            action: 'fetchTerms'
         }, () => this.fetchCurrentTerms());
      } else {
         this.showEmptyState('Error loading terms. Please try again.');
      }
   }
   /**
    * Update fields when taxonomy items are updated
    */
   updateFieldsForTaxonomy(taxonomy, items) {
      this.fields.forEach(field => {
         if (field.taxonomy === taxonomy && field.selectedTerms.size > 0) {
            // Update display with fresh term data
            field.selectedTerms.forEach(termId => {
               const term = items.find(item => item.id === termId);
               if (term) {
                  const selectedItem = field.selectedContainer.querySelector(`[data-id="${termId}"]`);
                  if (selectedItem) {
                     selectedItem.dataset.path = term.path;
                     selectedItem.querySelector('span').textContent = term.path;
                  }
               }
            });
         }
      }, {
         root: this.ui.terms.sentinel,
         threshold: 0.5
      });
      this.clickHandler = this.handleClick.bind(this);
      this.changeHandler = this.handleChange.bind(this);
      this.inputHandler = this.handleInput.bind(this);
      this.focusHandler = this.handleFocus.bind(this);
      this.blurHandler = this.handleBlur.bind(this);
      document.addEventListener('click', this.clickHandler);
      document.addEventListener('change', this.changeHandler);
      document.addEventListener('input', this.inputHandler);
      document.addEventListener('focus', this.focusHandler, true);
      document.addEventListener('blur', this.blurHandler, true);
   }
   /**
    * Scan page for existing taxonomy fields and register them
    */
   scanExistingFields() {
      const selectors = document.querySelectorAll('.field.taxonomy, .field.post');
      selectors.forEach(selector => {
         try {
            this.registerField(selector);
         } catch (error) {
            this.error.log(error, {
               component: 'TaxonomySelector',
               action: 'scanExistingFields',
               container: selector.dataset.name
            });
   handleClick(e) {
      if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
         return;
      }
      const fieldId = this.getFieldId(e.target) || this.activeField;
      const field = this.fields.get(fieldId);
      if (!fieldId || !field) return;
      if (this.creator) {
         let button = window.targetCheck(e, this.selectors.create.button);
         if (button) {
            this.maybeCreateTerm(e).then(()=>{});
         }
      });
   }
      }
   /**
    * Register a taxonomy field
    */
   registerField(field, options = {}) {
      let input = field.querySelector('input[type=hidden]');
      if (!input) {
      const removeButton = window.targetCheck(e, '.remove-term');
      if (removeButton) {
         const termId = removeButton.closest('[data-id]').dataset.id??false;
         if (fieldId && termId) {
            this.removeSelected(parseInt(termId), fieldId);
         }
         return;
      }
      if (!('fieldId' in field.dataset)) {
         field.dataset.fieldId = this.createFieldId(field);
      }
      let fieldId = field.dataset.fieldId;
      let button = field.querySelector('button.taxonomy-toggle');
      const autocomplete = window.targetCheck(e, '.item.autocomplete');
      let config = {
      if (autocomplete) {
         let termId = parseInt(autocomplete.dataset.id);
         this.addSelected(termId, fieldId);
         this.scheduleHideDropdown(fieldId, 6000);
         if (field.ui.search) {
            field.ui.search.value = '';
         }
         return;
      }
      const toggleButton = window.targetCheck(e, this.selectors.field.toggle);
      if (toggleButton) {
         e.preventDefault();
         this.openModal(fieldId);
         return;
      }
      if (e.target.matches('.modal-close')) {
         this.updateFieldValue(fieldId);
         this.modal?.handleClose();
         return;
      }
      const backToParent = window.targetCheck(e, this.selectors.nav.back);
      if (backToParent) {
         this.navigateToParent();
         return;
      }
      const toChild = window.targetCheck(e, this.selectors.nav.child);
      if (toChild) {
         const termItem = e.target.closest('li');
         const termId = parseInt(termItem.dataset.id);
         if (termId) {
            this.navigateTo(termId);
         }
         return;
      }
      const pathLevel = window.targetCheck(e, this.selectors.nav.pathLevel);
      if (pathLevel) {
         const termId = parseInt(pathLevel.dataset.id)??0;
         this.navigateTo(termId);
         return;
      }
      const dropdown = window.targetCheck(e, this.selectors.field.dropdown);
      if (dropdown) {
         // reset the timer for hiding the dropdown
         this.scheduleHideDropdown(fieldId);
         return;
      }
      const clearSearch = window.targetCheck(e, this.selectors.search.clear);
      if (clearSearch) {
         const field = this.currentField();
         if (field && field.ui.search) {
            field.ui.search.value = '';
            this.store.setFilters({
               search: '',
               page: 1,
               parent: this.store.filters.parent || 0
            });
         }
         if (this.ui.search.input) {
            this.ui.search.input.value = '';
         }
      }
   }
   handleChange(e) {
      if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
         return;
      }
      if (!['checkbox', 'button'].includes(e.target.type)) return;
      e.preventDefault();
      e.stopPropagation();
      const termId = parseInt(e.target.dataset.id);
      let fieldId = this.getFieldId(e.target);
      if (e.target.checked) {
         this.addSelected(termId, fieldId);
      } else {
         this.removeSelected(termId, fieldId);
      }
   }
   //For search in modal or field autocomplete
   handleInput(e) {
      if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
         return;
      }
      let fieldId = this.getFieldId(e.target)??this.activeField;
      if (!fieldId) return;
      const field = this.fields.get(fieldId);
      if (!field) return;
      if (['checkbox', 'button'].includes(e.target.type)) return;
      e.preventDefault();
      e.stopPropagation();
      //If it's the autocomplete field, we need to set the active field
      if (!this.container.open) {
         this.setField(fieldId);
      }
      let query = e.target.value.trim();
      this.setMessage(field,true, `Searching for "${query}" in ${field.plural??'items'}`);
      window.debouncer.schedule(
         `${fieldId}-search`,
         async () => {
            if (this.container.open) {
               window.removeChildren(this.ui.terms.list);
            }
            await this.store.setFilters({
               taxonomy: field.taxonomy,
               search: query,
               page: 1,
               parent: query ? 0 : (this.store.filters.parent || 0)
            });
         },
         100
      );
   }
   setField(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field) {
         console.error('No field found...');
         return;
      }
      this.activeField = fieldId;
      this.setMessage(field,true, `Loading ${field.plural}...`);
      this.resetFilters({taxonomy: field.taxonomy});
   }
   resetFilters(filters) {
      if (!Object.hasOwn(filters, 'taxonomy')) {
         return;
      }
      filters = {
         page: 1,
         search: '',
         parent: 0,
         ... filters
      };
      this.store.setFilters(filters);
   }
   handleFocus(e) {
      if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
         return;
      }
      const fieldId = this.getFieldId(e.target);
      if (!fieldId) return;
      const field = this.fields.get(fieldId);
      if (!field) return;
      if (!field.hasAutocomplete && !field.hasSearch) return;
      window.debouncer.cancel(`${fieldId}-search-results`);
      if (!this.container.open){
         this.setField(fieldId);
      }
   }
   //Hide autocomplete dropdown on blur
   handleBlur(e) {
      if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
         return;
      }
      const fieldId = this.getFieldId(e.target);
      if (!fieldId) return;
      const field = this.fields.get(fieldId);
      if (!field) return;
      if (!field.hasAutocomplete || this.container.open) return;
      if (e.target.closest('.remove-item')) return;
      if (e.relatedTarget && field.ui.dropdown.wrapper?.contains(e.relatedTarget)) return;
      this.scheduleHideDropdown(fieldId);
   }
   scheduleHideDropdown(fieldId, delay = 1500){
      const field = this.fields.get(fieldId);
      if (!field) return;
      window.debouncer.schedule(
         `${fieldId}-search-results`,
         () => {
            if (!this.container.open) {
               this.activeField = null;
            }
            if (field.ui.dropdown.wrapper) {
               field.ui.dropdown.wrapper.hidden = true;
            }
         },
         delay
      );
   }
   /******************************************************************
    MODAL
    ******************************************************************/
   initModal() {
      this.modalID = 'dialog#jvb-selector';
      this.container = document.querySelector(this.modalID);
      this.modal = new window.jvbModal(
         this.container,
         {
            handleForm: false,
            open: null
         }
      );
      this.modal.subscribe((event, data) => {
         switch (event) {
            case 'modal-close':
               this.closeModal()
               break;
         }
      });
   }
   toggleModal(fieldId, open = true) {
      const field = this.fields.get(fieldId);
      if (!field) return;
      if (open) {
         this.openModal(fieldId);
      } else {
         this.closeModal();
      }
   }
   openModal(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field) return;
      this.setField(fieldId);
      this.ui.modal.title.textContent = (field.isFilter) ?`Filter by ${field.singular}` : `Select ${field.plural}`;
      if (this.ui.search.container) {
         this.ui.search.container.hidden = !field.canSearch;
      }
      if (this.creator) {
         this.creator.handleOpen(field);
      }
      let message = `Opened ${field.singular} selection. Choose from checkboxes, or search to filter results.`;
      window.removeChildren(this.ui.selected);
      window.removeChildren(this.ui.terms.list);
      this.modal.handleOpen();
      this.a11y.announce(message);
   }
   openEmpty(taxonomy, singular, plural, onComplete) {
      // Store the callback for when modal closes
      this.emptyCallback = onComplete;
      // Create a temporary "field" for bulk operations
      const bulkFieldId = `empty-${taxonomy}-${Date.now()}`;
      if (!this.fields.has(bulkFieldId)) {
         this.fields.set(bulkFieldId, {
            id: bulkFieldId,
            taxonomy: taxonomy,
            singular: singular,
            plural: plural,
            canSearch: true,
            canCreate:  false,
            hasAutocomplete: false,
            isFilter: false,
            isEmpty: true,
            limit: 0,
            ui: {},
            element: null,
            value: null,
            toggle: null,
            checked: true
         });
         this.selectedTerms.set(bulkFieldId, new Set());
      }
      this.setField(bulkFieldId);
      this.ui.modal.title.textContent = `Add to ${plural}`;
      if (this.ui.search?.container) {
         this.ui.search.container.hidden = false;
      }
      window.removeChildren(this.ui.selected);
      window.removeChildren(this.ui.terms.list);
      this.modal.handleOpen();
   }
   closeModal() {
      const field = this.fields.get(this.activeField);
      if (!field) return;
      this.updateFieldValue(this.activeField);
      this.observer.unobserve(this.ui.terms.sentinel);
      window.removeChildren(this.ui.terms.list);
      if (field.isEmpty  && this.emptyCallback) {
         const selectedTermIds = Array.from(this.selectedTerms.get(this.activeField) || []);
         const selectedTerms = selectedTermIds.map(id => this.store.get(id)).filter(Boolean);
         this.emptyCallback({
            taxonomy: field.taxonomy,
            termIds: selectedTermIds,
            terms: selectedTerms
         });
         // Cleanup temporary bulk field
         this.fields.delete(this.activeField);
         this.selectedTerms.delete(this.activeField);
         this.emptyCallback = null;
         this.bulkAssignmentTaxonomy = null;
      } else {
         this.notify('selected-terms', {
            terms: this.selectedTerms.get(this.activeField),
            taxonomy: field.taxonomy
         });
      }
      this.activeField = null;
      let message = `Closed ${field.singular} selector.`;
      this.a11y.announce(message);
   }
   navigateToParent() {
      const current = this.store.filters.parent;
      if (current === 0) return;
      let term = this.store.get(parseInt(current));
      if (!term) {
         this.navigateTo(0);
         return;
      }
      let parent = term.parent;
      this.navigateTo(parseInt(parent));
   }
   navigateTo(termId = 0) {
      termId = parseInt(termId)??0;
      this.store.setFilters({parent: termId, page: 1});
      window.removeChildren(this.ui.terms.list);
      this.updateBreadcrumbs(termId);
   }
   nextPage() {
      let current = this.store.filters.page;
      let page = Math.min(current++, this.store.lastResponse.total);
      this.store.setFilters({page:page});
   }
   prevPage() {
      let current = this.store.filters.page;
      let page = Math.max(current - 1, 1);
      this.store.setFilters({page:page});
   }
   addTermToModal(termId) {
      const term = this.store.get(termId);
      if (!term) return;
      const field = this.currentField();
      if (!field) return;
      if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
      this.ui.selected.append(this.getSelectedTermUI(term));
   }
   getSelectedTermUI(term, showPath = true) {
      return window.jvbTemplates.create('selectedTerm', term);
   }
   /******************************************************************
    FIELDS
    ******************************************************************/
   scanExistingFields(container = document.body) {
      container.querySelectorAll('[data-type="selector"], [data-field-type="selector"]').forEach(
         selector => {
            try {
               if (selector.dataset.lazy) {
                  this.lazyInit = true;
               } else {
                  // Register field if not already registered
                  // registerField will check if already registered and return early if so
                  this.registerField(selector);
               }
            } catch (error) {
               this.error.log(error, {
                  component: 'TaxonomySelector',
                  action: 'scanExistingFields',
                  container: selector.dataset.name
               });
            }
         }
      );
      if (this.lazyInit) {
         this.initObserver(container);
      }
   }
   unregisterFields(container) {
      container.querySelectorAll('[data-type="selector"],[data-field-type="selector"]').forEach(
         selector=> {
            this.fields.delete(selector.dataset.fieldId);
         }
      );
   }
   initObserver(container){
      this.lazyObserver = new IntersectionObserver((entries) => {
         entries.forEach(entry => {
            if (entry.isIntersecting && entry.target.dataset.lazy) {
               delete entry.target.dataset.lazy;
               this.registerField(entry.target);
               this.lazyObserver.unobserve(entry.target);
            }
         });
      }, {rootMargin: '50px'});
      container.querySelectorAll('[data-type="selector"][data-lazy], [data-field-type="selector"][data-lazy]').forEach(field => {
         this.lazyObserver.observe(field);
      });
   }
   registerField(element, options = {}) {
      if (element.dataset.fieldId && this.fields.has(element.dataset.fieldId)) {
         return element.dataset.fieldId; // Already registered
      }
      let input = element.querySelector('input[type="hidden"]');
      if (!input && !Object.hasOwn(element.dataset, 'filter')) {
         return;
      }
      if (!('fieldId' in element.dataset)) {
         element.dataset.fieldId = window.generateID('selector');
      }
      const fieldId = element.dataset.fieldId;
      let selectors = this.selectors.field;
      const isFilter = Object.hasOwn(element.dataset,'filter') && element.dataset.filter === 'taxonomy';
      let button = (isFilter) ? element : element.querySelector('button.selector-toggle');
      if (Object.keys(options).length === 0){
         if (!button) return;
         options = {
            taxonomy: button.dataset.taxonomy,
            single: button.dataset.single,
            plural: button.dataset.plural,
            search: Object.hasOwn(button.dataset, 'search'),
            autocomplete: Object.hasOwn(button.dataset, 'autocomplete'),
            creatable: Object.hasOwn(button.dataset, 'creatable')
         };
      } else if (Object.hasOwn(options, 'toggle')) {
         button = document.querySelector(options.toggle);
         selectors.toggle = options.toggle;
      }
      const config = {
         id: fieldId,
         input: input,
         container: field,
         taxonomy: button.dataset.taxonomy,
         name: field.dataset.field,
         maxSelection: parseInt(button.dataset.max) || 0,
         canSearch: 'search' in button.dataset,
         canCreate: 'creatable' in button.dataset,
         isRequired: 'required' in button.dataset,
         selectedTerms: new Set(),
         value: input,
         element: element,
         taxonomy: options.taxonomy??false,
         singular: options.single??'',
         plural: options.plural??'',
         name: element.dataset.field,
         canSearch: options.search??false,
         limit: options.limit??0,
         hasAutocomplete: options.autocomplete??false,
         canCreate: options.creatable??false,
         isRequired: options.required??false,
         isFilter: isFilter,
         toggle: button,
         selectedContainer: field.querySelector('.selected-items'),
         ...options
         create: {
            button: null,
            span: null
         },
         selectors: selectors,
         ui: window.uiFromSelectors(selectors, element),
         checked: false,
      };
      // Parse initial selected values
      const value = input.value.trim();
      if (value !== '') {
         const selectedIds = value.split(',')
            .map(id => parseInt(id.trim()))
            .filter(id => !isNaN(id));
         selectedIds.forEach(id => config.selectedTerms.add(id));
      if (isFilter && !config.ui.toggle) {
         config.ui.toggle = element;
      }
      if (!config.taxonomy) {
         console.error('TaxonomySelector: Field missing taxonomy', element);
         return;
      }
      if (!config.singular || !config.plural) {
         console.warn('TaxonomySelector: Field missing singular/plural labels', element);
         config.singular = config.taxonomy.replace('jvb_', '');
         config.plural = config.singular + 's';
      }
      this.fields.set(fieldId, config);
      // Ensure store exists for this taxonomy
      this.getOrCreateStore(config.taxonomy);
      //Check for stored selected terms in hidden input
      this.setSelectedFromValue(fieldId, input);
      // Initialize display for any pre-selected values
      if (config.selectedTerms.size > 0) {
         this.initFieldDisplay(fieldId);
      if (this.isInitializing) {
         this.batchFetch.add(config.taxonomy);
      }
      if (element.offsetParent !== null) {
         this.updateFieldUI(fieldId);
      } else {
         // Defer until visible
         requestIdleCallback(() => {
            if (element.offsetParent !== null) {
               this.updateFieldUI(fieldId);
            }
         }, {timeout: 2000});
      }
      return fieldId;
   }
   /**
    * Create unique field ID
    */
   createFieldId(field) {
      this.index++;
      return 'selector-' + this.index;
   setSelectedFromValue(fieldId, input) {
      if (!fieldId) return;
      let field = this.fields.get(fieldId);
      if (!field) return;
      if (!input && !field.isFilter) return;
      let selected = new Set();
      if (input) {
         input.value.trim()
            .split(',')
            .map(id => parseInt(id.trim()))
            .filter(id => !isNaN(id))
            .forEach(id => selected.add(id));
      }
      this.selectedTerms.set(fieldId, selected);
   }
   /**
    * Initialize display for a field with existing values
    */
   async initFieldDisplay(fieldId) {
   addSelected(termId, fieldId = null) {
      if (!fieldId) fieldId = this.activeField;
      const field = this.fields.get(fieldId);
      if (!field || field.selectedTerms.size === 0) return;
      const term = this.store.get(termId);
      if (!field || !term) return;
      const store = this.getOrCreateStore(field.taxonomy);
      const selectedIds = Array.from(field.selectedTerms);
      const selected = this.selectedTerms.get(fieldId);
      if (field.limit !== 0 && selected.size >= field.limit) return;
      // Check store for cached terms first
      const cachedTerms = [];
      const needsFetch = [];
      selected.add(parseInt(termId));
      if (!this.container.open && !field.isFilter) {
         this.updateFieldValue(fieldId);
      }
      this.addTermToDisplay(termId, fieldId);
      this.checkLimits(fieldId);
   }
   removeSelected(termId, fieldId = null) {
      if (!fieldId) fieldId = this.activeField;
      const field = this.fields.get(fieldId);
      const term = this.store.get(termId);
      if (!field || !term) return;
      this.selectedTerms.get(fieldId).delete(parseInt(termId));
      selectedIds.forEach(termId => {
         const term = store.getItem(termId);
         if (term) {
            cachedTerms.push(term);
      const selectedItem = (field.ui.selected) ? field.ui.selected.querySelector(`[data-id="${termId}"]`) : false;
      if (selectedItem) selectedItem.remove();
      if (this.container.open) {
         let item = (this.ui.selected) ? this.ui.selected.querySelector(`[data-id="${termId}"]`) : false;
         if (item) item.remove();
         let checkbox = this.ui.terms.list.querySelector(`[type=checkbox][data-id="${termId}"]`);
         if (checkbox) {
            checkbox.checked = false;
         }
      }
      if (!this.container.open && !field.isFilter) {
         this.updateFieldValue(fieldId);
      }
      this.checkLimits(fieldId);
   }
   updateFieldValue(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field) return;
      let selected = Array.from(this.selectedTerms.get(fieldId));
      if (field.ui.value) {
         field.ui.value.value = selected.join(',')??'';
         field.ui.value.dispatchEvent(new Event('change', { bubbles: true }));
      }
   }
   checkLimits(fieldId) {
      if (!this.container.open) return;
      const field = this.fields.get(fieldId);
      if (!field || !field.isFilter || field.limit === 0) return;
      const disabled = this.selectedTerms.get(fieldId).size >= field.limit;
      this.setCheckboxes(disabled);
   }
   updateFieldFromInput(input) {
      const fieldId = this.getFieldId(input);
      if (!fieldId) return;
      const field = this.fields.get(fieldId);
      if(!field) return;
      this.setSelectedFromValue(fieldId, input);
      this.updateFieldUI(fieldId);
   }
   updateFieldUI(fieldId) {
      const field = this.fields.get(fieldId);
      let selected = this.selectedTerms.get(fieldId)??new Set();
      if (!field || field.isFilter || selected.size === 0) return;
      Array.from(selected).forEach(termId => {
         this.addTermToDisplay(termId, fieldId);
      });
   }
   updateFieldsForTaxonomy(taxonomy) {
      let fields = Array.from(this.fields.values())
         .filter(field => field.taxonomy === taxonomy);
      const hasItems = Array.from(this.store.data.values())
         .some(term => term && term.taxonomy === taxonomy);
      fields.forEach(field => {
         if (!field.toggle) return;
         field.toggle.disabled = !hasItems && !field.canCreate;
         field.toggle.title = !hasItems
            ? `No ${field.singular} available`
            : `Select ${field.plural}`;
         field.checked = true;
      });
   }
   showModalTerms(showPath = false) {
      const field = this.currentField();
      const terms = this.store.getFiltered();
      if (terms.length === 0) {
         if (this.store.filters.page??1 === 1) {
            window.removeChildren(this.ui.terms.list);
         }
         this.setMessage(field,true, this.store.filters.search === ''
            ? `No matching ${field.plural}.`
            : `No ${field.plural} found.`,
            false);
         if (this.ui.terms.sentinel) {
            this.observer.unobserve(this.ui.terms.sentinel);
         }
         return;
      }
      this.setCreateButton(field,true);
      if (this.ui.terms.sentinel) {
         if (this.store.lastResponse?.has_more) {
            this.observer.observe(this.ui.terms.sentinel);
         } else {
            needsFetch.push(termId);
         }
      });
      // Display cached terms immediately
      cachedTerms.forEach(term => {
         this.addTermToDisplay(fieldId, term.id, term.name, term.path);
      });
      // Fetch missing terms if needed
      if (needsFetch.length > 0) {
         try {
            const response = await store.fetch('terms', {
               filters: {
                  taxonomy: field.taxonomy,
                  termIDs: needsFetch.join(',')
               }
            });
            if (response.terms) {
               response.terms.forEach(term => {
                  store.setItem(term.id, term);
                  this.addTermToDisplay(fieldId, term.id, term.name, term.path);
               });
            }
         } catch (error) {
            console.error('Failed to fetch missing terms:', error);
         }
      }
   }
   /**
    * Initialize modal elements
    */
   initModal() {
      this.modalID = 'dialog#jvb-selector';
      this.modal = document.querySelector(this.modalID);
      if (!this.modal) {
         console.warn('Taxonomy selector modal not found');
         return;
      }
      this.initModalElements();
      // Initialize modal instance
      this.modalInstance = new window.jvbModal(this.modal, {
         handleForm: false,
         save: null,
         open: null
      });
      this.modalInstance.subscribe((event, data) => {
         switch (event) {
            case 'modal-open':
               console.log(data);
               this.openModal(data);
               break;
            case 'modal-close':
               this.closeModal(data);
               break;
         }
      });
   }
   /**
    * Initialize modal element references
    */
   initModalElements() {
      this.selectors = {
         search: {
            input: '[type=search]',
            clear: '.clear-search',
            container: '.search-wrapper'
         },
         termsList:  '.items-container',
         termsWrap:  '.items-wrap',
         breadcrumbs: {
            nav: 'nav.term-navigation',
            back: '.back-to-parent',
         },
         loading: {
            loading: '.loading',
            text: '.loading span'
         },
         selectedTerms: '.selected-items',
         sentinel: '.scroll-sentinel',
         modal: {
            title: '#modal-title',
            content: '.modal-content'
         },
         create: {
            details: '.create-new-term',
            parent: '#select_parent',
            summary: '.create-new-term summary',
            name: '#term_name',
            button: '.submit-term',
            label: {
               name: '[for=term_name]',
               parent: '[for=select_parent]'
            }
         },
         favouriteTerms: '.favourite-terms'
      }
      this.ui = window.uiFromSelectors(this.selectors);
      // Initialize intersection observer for infinite scroll
      this.observer = new IntersectionObserver((entries) => {
         entries.forEach(entry => {
            if (entry.isIntersecting && this.activeStore) {
               this.loadMoreTerms();
            }
         });
      }, {
         root: this.ui.termsWrap,
         threshold: 0.5
      });
   }
   /**
    * Set up global event delegation
    */
   initGlobalListeners() {
      document.addEventListener('click', this.handleClick.bind(this));
      document.addEventListener('change', this.handleChange.bind(this));
   }
   /**
    * Handle global click events
    */
   handleClick(e) {
      // Handle taxonomy toggle buttons
      const toggleButton = window.targetCheck(e, '.taxonomy-toggle');
      if (toggleButton) {
         e.preventDefault();
         this.handleToggleClick(toggleButton);
         return;
      }
      // Handle remove selected term buttons
      const removeButton = window.targetCheck(e, 'button.remove-item');
      if (removeButton && e.target.closest('.jvb-selector')) {
         const fieldId = this.getFieldId(removeButton);
         const termId = removeButton.closest('.selected-item').dataset.id;
         this.removeSelectedTerm(fieldId, termId);
         return;
      }
      // Handle modal close button
      if (e.target.matches('.modal-close')) {
         if (this.modalInstance) {
            this.modalInstance.handleClose();
         }
         return;
      }
      // Handle clicks within the modal
      if (this.modal && this.modal.contains(e.target)) {
         this.handleModalClick(e);
      }
   }
   /**
    * Handle global change events
    */
   handleChange(e) {
      // Handle hidden input changes for taxonomy fields
      const taxonomyField = window.targetCheck(e, '.taxonomy.field, .post.field');
      if (taxonomyField && e.target.type === 'hidden') {
         const fieldId = this.getFieldId(e.target);
         this.updateFieldFromInput(fieldId);
         return;
      }
      // Handle modal changes
      if (this.modal && this.modal.contains(e.target)) {
         this.handleModalChange(e);
      }
   }
   /**
    * Handle toggle button click
    */
   handleToggleClick(toggle) {
      try {
         const fieldId = this.getFieldId(toggle);
         const field = this.fields.get(fieldId);
         if (!field) {
            console.error('Field not found for toggle:', fieldId);
            return;
         }
         this.setActiveField(fieldId);
         this.modalInstance.handleOpen();
      } catch (error) {
         console.error('Error handling toggle click:', error);
         this.error?.handleError(error, {
            component: 'TaxonomySelector',
            action: 'handleToggleClick'
         });
      }
   }
   /**
    * Set the active field for modal operations
    */
   setActiveField(fieldId) {
      this.activeField = fieldId;
      this.currentConfig = this.fields.get(fieldId);
      console.log('Current Taxonomy:',this.currentConfig.taxonomy);
      console.log('Labels: ',jvbSettings.labels[this.currentConfig.taxonomy]);
      this.currentSingular = jvbSettings.labels[this.currentConfig.taxonomy].single;
      this.currentPlural = jvbSettings.labels[this.currentConfig.taxonomy].plural;
      // Get or create store for this taxonomy
      this.activeStore = this.getOrCreateStore(this.currentConfig.taxonomy);
      // Clear modal selection state
      this.selectedTerms.clear();
      // Copy field's current selections to modal state
      if (this.currentConfig.selectedTerms) {
         this.currentConfig.selectedTerms.forEach(termId => {
            const term = this.activeStore.getItem(termId);
            if (term) {
               this.selectedTerms.set(termId, {
                  id: termId,
                  name: term.name,
                  path: term.path
               });
            } else {
               // If not in store, create minimal entry
               this.selectedTerms.set(termId, {
                  id: termId,
                  name: `Term ${termId}`,
                  path: `Term ${termId}`
               });
            }
         });
      }
   }
   /**
    * Handle clicks within modal
    */
   handleModalClick(e) {
      if (window.targetCheck(e, '.remove-item')) {
         let selectedItem = window.targetCheck(e, '.selected-item');
         if (selectedItem) {
            this.removeSelectedTermFromModal(selectedItem.dataset.id);
         }
      } else if (window.targetCheck(e, '.back-to-parent')) {
         this.navigateToParent();
      } else if (window.targetCheck(e, '.toggle-children')) {
         let termItem = e.target.closest('li');
         this.navigateToChild(
            parseInt(termItem.dataset.id),
            termItem.querySelector('.term-name').textContent
         );
      } else if (window.targetCheck(e, '.path-level')) {
         let pathLevel = window.targetCheck(e, '.path-level');
         this.navigateToPath(pathLevel);
      }
   }
   /**
    * Handle changes within modal (checkboxes)
    */
   handleModalChange(e) {
      if (window.targetCheck(e, this.modalID) && e.target.type === 'checkbox') {
         e.preventDefault();
         e.stopPropagation();
         const termId = parseInt(e.target.closest('li').dataset.id);
         const label = e.target.closest('li').querySelector('label');
         if (e.target.checked) {
            this.addSelectedTermToModal(termId, label.title, label.dataset.path);
         } else {
            this.removeSelectedTermFromModal(termId);
         }
      }
   }
   /**
    * Open modal for filtering (without a field)
    * @param {string} taxonomy - The taxonomy to filter by
    * @param {Function} callback - Callback when terms are selected
    * @param {Array} preselected - Array of term IDs already selected
    */
   openForFilter(taxonomy, callback, preselected = []) {
      // Create a temporary virtual field config
      const virtualFieldId = `filter-${taxonomy}-${Date.now()}`;
      this.fields.set(virtualFieldId, {
         id: virtualFieldId,
         input: null, // No input for filter mode
         container: null,
         taxonomy: taxonomy,
         name: `filter_${taxonomy}`,
         maxSelection: 0, // No limit for filters
         canSearch: true,
         canCreate: false, // Disable creation for filters
         isRequired: false,
         selectedTerms: new Set(preselected),
         toggle: null,
         selectedContainer: null,
         isFilterMode: true, // Flag for filter mode
         filterCallback: callback // Store the callback
      });
      this.setActiveField(virtualFieldId);
      this.modalInstance.handleOpen();
   }
   /**
    * Open modal and initialize
    */
   openModal() {
      if (!this.activeField || !this.currentConfig) {
         console.error('No active field set for modal');
         return;
      }
      this.resetModalState();
      this.updateModalForTaxonomy();
      // Reset store filters to default state
      this.activeStore.clearFilters();
      // Set up search if enabled
      if (this.currentConfig.canSearch) {
         this.ui.search.input.focus();
         this.searchHandler = window.debounce(() => this.handleSearch(), 300);
         this.ui.search.input.addEventListener('input', this.searchHandler);
      }
      // Initialize creator if available
      if (this.currentConfig.canCreate && 'jvbTaxCreator' in window) {
         this.creator = new window.jvbTaxCreator(this);
      }
      // Display current selections
      this.updateModalSelections();
      // Start observing for infinite scroll
      this.observer.observe(this.ui.sentinel);
      // Fetch initial terms
      this.fetchCurrentTerms();
   }
   /**
    * Close modal and save selections
    */
   closeModal() {
      this.observer.unobserve(this.ui.sentinel);
      window.removeChildren(this.ui.termsList);
      if (this.currentConfig?.isFilterMode) {
         // Call the filter callback with selected terms
         if (this.currentConfig.filterCallback) {
            const selectedIds = Array.from(this.selectedTerms.keys());
            this.currentConfig.filterCallback(selectedIds, this.currentConfig.taxonomy);
         }
         // Clean up the virtual field
         this.fields.delete(this.activeField);
      } else if (this.activeField) {
         this.saveSelectionsToField(this.activeField);
      }
      // Cleanup
      if (this.currentConfig?.canSearch && this.searchHandler) {
         this.ui.search.input.removeEventListener('input', this.searchHandler);
      }
      if (this.creator) {
         delete this.creator;
      }
      this.activeStore = null;
      this.activeField = null;
      this.currentConfig = null;
   }
   /**
    * Reset modal state
    */
   resetModalState() {
      this.disabled = false;
      window.removeChildren(this.ui.termsList);
      window.removeChildren(this.ui.selectedTerms);
      this.ui.search.input.value = '';
      // Clear navigation breadcrumbs
      window.removeChildren(this.ui.breadcrumbs.nav);
      this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back);
      this.ui.breadcrumbs.back.hidden = true;
   }
   /**
    * Update modal content for current taxonomy
    */
   updateModalForTaxonomy() {
      if (!this.currentConfig) return;
      this.ui.modal.title.textContent = `Select ${this.currentPlural}`;
      if (this.ui.search.container) {
         this.ui.search.container.style.display = this.currentConfig.canSearch ? 'block' : 'none';
      }
      if (this.ui.create.details) {
         this.ui.create.details.style.display = this.currentConfig.canCreate ? 'block' : 'none';
         this.ui.create.details.hidden = !this.currentConfig.canCreate;
         if (this.ui.create.summary) {
            this.ui.create.summary.textContent = `Add new ${this.currentSingular}`;
         }
         if (this.ui.create.label.name) {
            this.ui.create.label.name.textContent = `Name this ${this.currentSingular}`;
         }
         if (this.ui.create.label.parent) {
            this.ui.create.label.parent.textContent = `Nest it under`;
         }
         if (this.ui.create.parent) {
            this.observer.unobserve(this.ui.terms.sentinel);
         }
      }
      const openMessage = `Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;
      this.a11y?.announce(openMessage);
      const currentParent = this.store.filters.parent??0;
      this.ui.nav.back.hidden = currentParent === 0;
      window.chunkIt(
         terms,
         (term) => this.createTermElement({show:showPath, ... term}),
         (fragment) => this.ui.terms.list.append(fragment),
         10
      ).then(()=>{});
      if (terms.length > 0) {
         this.setMessage(field,false);
      }
   }
   createTermElement(term) {
      if (!term || !term.name) return null;
      return window.jvbTemplates.create('termListItem', term);
   }
   /**
    * Update modal selections display
    */
   updateModalSelections() {
      window.removeChildren(this.ui.selectedTerms);
   showAutocompleteTerms() {
      const field = this.currentField();
      if (!field || !field.hasAutocomplete || !field.ui.dropdown?.list) return;
      const dropdown = field.ui.dropdown.list;
      const terms = this.currentTerms();
      this.selectedTerms.forEach((termData, id) => {
         this.addTermToModalDisplay(id, termData.name, termData.path);
      });
      window.removeChildren(dropdown);
      if (terms.length === 0) {
         this.setMessage(field,true, `No ${field.plural} found.`, false);
      } else {
         window.chunkIt(
            terms,
            (term) => this.createAutocompleteTerm(term),
            (fragment) => dropdown.append(fragment)
         ).then(()=>{});
      this.checkSelectionLimits();
   }
         this.setMessage(field,false);
      }
      this.setCreateButton(field,true);
   /**
    * Add selected term to modal
    */
   addSelectedTermToModal(id, name, path) {
      this.selectedTerms.set(id, {
         id: id,
         name: name,
         path: path
      });
      this.addTermToModalDisplay(id, name, path);
      this.checkSelectionLimits();
      // Check the corresponding checkbox
      const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
      if (checkbox) {
         checkbox.checked = true;
      if (field.ui.dropdown.wrapper) {
         field.ui.dropdown.wrapper.hidden = false;
      }
   }
   /**
    * Remove selected term from modal
    */
   removeSelectedTermFromModal(id) {
      this.selectedTerms.delete(parseInt(id));
   createAutocompleteTerm(term) {
      return window.jvbTemplates.create('autocompleteItem', term);
   }
   /******************************************************************
    UI
    ******************************************************************/
   addTermToDisplay(termId, fieldId) {
      const term = this.store.get(termId);
      const field = this.fields.get(fieldId);
      if (!term || !field) return;
      // Remove from modal display
      const selectedItem = this.ui.selectedTerms.querySelector(`[data-id="${id}"]`);
      if (selectedItem) {
         selectedItem.remove();
      //if the term already exists in the selected items, bail early
      if (field.ui.selected && field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
      let item = this.getSelectedTermUI(term);
      if (field.ui.selected) {
         field.ui.selected.append(item);
      }
      // Uncheck the corresponding checkbox
      const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
      if (checkbox) {
         checkbox.checked = false;
      if (this.container.open) {
         this.addTermToModal(termId);
         const checkbox = this.ui.terms.list.querySelector(`input[value="${termId}"]`);
         if (checkbox) checkbox.checked = true;
      }
   }
   updateBreadcrumbs(termId) {
      const nav = this.ui.nav.nav;
      if (!nav) return;
      const existingCrumb = Array.from(nav.children)
         .find(crumb => parseInt(crumb.dataset.id) === termId);
      if (existingCrumb) {
         // Remove all siblings after this crumb
         let nextSibling = existingCrumb.nextElementSibling;
         while (nextSibling) {
            const toRemove = nextSibling;
            nextSibling = nextSibling.nextElementSibling;
            toRemove.remove();
         }
      } else {
         // Add new breadcrumb
         const term = this.store.get(termId);
         if (!term) return;
         const crumb = window.jvbTemplates.create('termBreadcrumb', term);
         nav.append(crumb);
      }
   }
   updateSelectionCount() {
      if (!this.container.open) return;
      const field = this.fields.get(this.activeField);
      if (!field) return;
      if (this.ui.modal.count) {
         const total = this.selectedTerms.get(this.activeField).size;
         this.ui.modal.count.textContent = field.limit > 0
            ? `${total} of ${field.limit} ${field.plural} selected`
            : `${total} ${field.plural} selected`;
      }
      this.checkSelectionLimits();
   }
   /******************************************************************
    UTILITY
    ******************************************************************/
   checkRendered(collection, term) {
      if (!collection) return;
      if (!Object.hasOwn(collection, term.taxonomy)) {
         collection[term.taxonomy] = new Map();
      }
      return collection[term.taxonomy].has(term.id);
   }
   currentField() {
      return this.fields.get(this.activeField)??false;
   }
   currentTerms() {
      return this.store.getFiltered();
   }
   needsCreator() {
      return Array.from(this.fields.values()).some(field =>
         field.canCreate || field.hasAutocomplete
      );
   }
   getFieldId(element) {
      if (element.dataset.fieldId) return element.dataset.fieldId;
      const fieldContainer = element.closest('[data-field-id]');
      return fieldContainer?.dataset.fieldId || null;
   }
   /**
    * Add term to modal display
    */
   addTermToModalDisplay(id, name, path) {
      const item = window.getTemplate('selectedTerm').cloneNode(true);
      item.dataset.id = id;
      item.dataset.path = path;
      item.dataset.name = name;
      item.dataset.taxonomy = this.currentConfig.taxonomy;
      item.querySelector('span').textContent = path;
      item.querySelector('button').title = `Remove ${name}`;
      this.ui.selectedTerms.appendChild(item);
   }
   /**
    * Check selection limits and disable/enable checkboxes
    */
   checkSelectionLimits() {
      if (!this.currentConfig || this.currentConfig.maxSelection === 0) {
         return;
      }
      this.disabled = this.selectedTerms.size >= this.currentConfig.maxSelection;
      this.setCheckboxes(this.disabled);
   }
   /**
    * Set checkbox disabled state
    * Sets all checkbox disabled (or not)
    * @param {Boolean} disabled
    */
   setCheckboxes(disabled) {
      this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
      this.ui.terms.list.querySelectorAll('input[type=checkbox]').forEach(checkbox => {
         if (!checkbox.checked) {
            checkbox.disabled = disabled;
         }
      });
   }
   /**
    * Save modal selections to field
    */
   saveSelectionsToField(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field) return;
   /******************************************************************
    DATASTORE HELPERS
   ******************************************************************/
   handleStoreEvent(event, data) {
      const handlers = {
         'data-loaded': () => this.handleDataLoaded(),
         'filters-changed': () => this.handleFiltersChanged(data),
         'fetch-error': () => this.handleFetchError()
      };
      // Clear current field selections
      field.selectedTerms.clear();
      window.removeChildren(field.selectedContainer);
      // Add modal selections to field
      this.selectedTerms.forEach((termData, id) => {
         field.selectedTerms.add(id);
         this.addTermToDisplay(fieldId, id, termData.name, termData.path);
      });
      // Update hidden input
      const selectedIds = Array.from(field.selectedTerms);
      field.input.value = selectedIds.join(',');
      field.input.dispatchEvent(new Event('change', { bubbles: true }));
   }
   /**
    * Remove selected term from field
    */
   removeSelectedTerm(fieldId, termId) {
      const field = this.fields.get(fieldId);
      if (!field) return;
      const id = parseInt(termId);
      field.selectedTerms.delete(id);
      // Remove from display
      const selectedItem = field.selectedContainer.querySelector(`[data-id="${id}"]`);
      if (selectedItem) {
         selectedItem.remove();
      }
      // Update hidden input
      const selectedIds = Array.from(field.selectedTerms);
      field.input.value = selectedIds.join(',');
      field.input.dispatchEvent(new Event('change', { bubbles: true }));
   }
   /**
    * Add term to field display
    */
   addTermToDisplay(fieldId, id, name, path) {
      const field = this.fields.get(fieldId);
      if (!field || field.selectedContainer.querySelector(`[data-id="${id}"]`)) {
         return; // Already displayed
      }
      const item = window.getTemplate('selectedTerm').cloneNode(true);
      item.dataset.id = id;
      item.dataset.path = path;
      item.dataset.name = name;
      item.dataset.taxonomy = field.taxonomy;
      item.querySelector('span').textContent = path;
      item.querySelector('button').title = `Remove ${name}`;
      field.selectedContainer.appendChild(item);
   }
   /**
    * Update field from hidden input value
    */
   updateFieldFromInput(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field) return;
      const value = field.input.value.trim();
      field.selectedTerms.clear();
      window.removeChildren(field.selectedContainer);
      if (value !== '') {
         const selectedIds = value.split(',')
            .map(id => parseInt(id.trim()))
            .filter(id => !isNaN(id));
         selectedIds.forEach(id => field.selectedTerms.add(id));
         this.initFieldDisplay(fieldId);
      try {
         handlers[event]?.(data);
      } catch (error) {
         console.error(`Error handling store event "${event}":`, error);
      }
   }
   handleDataLoaded() {
      const taxonomy = this.store.filters.taxonomy;
   /**
    * Handle search input
    */
   handleSearch() {
      const query = this.ui.searchInput.value.trim();
      if (query.length >= 2 || query.length === 0) {
         // Reset pagination when searching
         this.activeStore.setFilter('page', 1);
         this.activeStore.setFilter('search', query);
         window.removeChildren(this.ui.termsList);
         if (query.length >= 2) {
            this.showLoading();
            this.fetchCurrentTerms();
         } else if (query.length === 0) {
            // Clear search and reload
            this.showLoading();
            this.fetchCurrentTerms();
         }
      } else {
         this.hideLoading();
         this.showEmptyState('Enter at least 2 characters to search.');
      }
   }
   /**
    * Navigate to parent term
    */
   navigateToParent() {
      const currentParent = this.activeStore.filters.parent || 0;
      // Find parent of current parent (could enhance this with breadcrumb tracking)
      this.activeStore.setFilter('parent', 0);
      this.activeStore.setFilter('page', 1);
      window.removeChildren(this.ui.termsList);
      this.showLoading();
      this.fetchCurrentTerms();
      // Update breadcrumbs
      this.ui.breadcrumbs.back.hidden = true;
   }
   /**
    * Navigate to child term
    */
   navigateToChild(termId, termName) {
      this.activeStore.setFilter('parent', termId);
      this.activeStore.setFilter('page', 1);
      window.removeChildren(this.ui.termsList);
      this.showLoading();
      this.fetchCurrentTerms();
      // Update breadcrumbs
      this.updateBreadcrumbs(termId, termName);
      this.ui.breadcrumbs.back.hidden = false;
   }
   /**
    * Navigate to specific path level
    */
   navigateToPath(pathLevel) {
      const parentId = parseInt(pathLevel.dataset.id) || 0;
      this.activeStore.setFilter('parent', parentId);
      this.activeStore.setFilter('page', 1);
      window.removeChildren(this.ui.termsList);
      this.showLoading();
      this.fetchCurrentTerms();
      // Update breadcrumbs to this level
      // You'd need to track the full path to properly implement this
      this.ui.breadcrumbs.back.hidden = parentId === 0;
   }
   /**
    * Fetch terms using current store filters
    */
   fetchCurrentTerms() {
      if (!this.activeStore) return;
      this.showLoading();
      this.activeStore.fetch();
   }
   /**
    * Load more terms (pagination)
    */
   loadMoreTerms() {
      if (!this.activeStore) return;
      const currentPage = this.activeStore.filters.page || 1;
      this.activeStore.setFilter('page', currentPage + 1);
      // fetch() will be called automatically by setFilter
   }
   /**
    * Render terms list
    */
   renderTerms(terms, append = false, showPath = false) {
      if (!append) {
         window.removeChildren(this.ui.termsList);
      if (taxonomy) {
         const taxonomies = taxonomy.split(',').map(t => t.trim());
         taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
      }
      if (terms.length === 0) {
         if (!append) {
            this.showEmptyState();
         }
      if (this.container.open) {
         this.showResults();
         return;
      }
      if (this.activeField) {
         this.showResults(true);
         return;
      }
   }
      // Update breadcrumbs if needed
      const currentParent = this.activeStore.filters.parent || 0;
      this.ui.breadcrumbs.back.hidden = currentParent === 0;
   showResults(isAutoComplete = false) {
      const terms = this.store.getFiltered();
      const filters = this.store.filters;
      const isSearch = filters.search && filters.search.length > 0;
      terms.forEach(term => {
         // Check if we have a cached DOM element
         const cachedElement = this.activeStore.getDOMElement(term.id, 'list-item');
      this.notify('terms-loaded', {
         terms,
         filters
      });
         if (cachedElement) {
            // Update checkbox state if needed
            const checkbox = cachedElement.querySelector('input[type="checkbox"]');
            if (checkbox) {
               checkbox.checked = this.selectedTerms.has(term.id);
               checkbox.disabled = !checkbox.checked && this.disabled;
      if (!this.activeField && isAutoComplete) {
         return;
      }
      this.setMessage(this.currentField(), false);
      if (isAutoComplete) {
         this.showAutocompleteTerms();
      } else {
         this.showModalTerms(isSearch);
      }
      this.a11y.announce(terms.length);
   }
   handleFiltersChanged(data) {
      //maybe do something?
   }
   handleFetchError(error) {
      const field = this.currentField();
      this.setMessage(field, true, 'Something went wrong.', false);
      const conf = this.container.open || field?.isFilter ? this.ui : field?.ui;
      const p = conf?.message?.message;
      if (p && !p.querySelector('.clear-cache-btn')) {
         const btn = document.createElement('button');
         btn.className = 'clear-cache-btn';
         btn.type = 'button';
         btn.textContent = 'Clear cache and try again';
         btn.addEventListener('click', async () => {
            btn.remove();
            this.store.clearCache();
            if (this.activeField && field) {
               await this.store.setFilters({
                  taxonomy: field.taxonomy,
                  page: 1,
                  search: '',
                  parent: 0
               });
            }
            this.ui.termsList.appendChild(cachedElement);
         } else {
            // Create new element and cache it
            const element = this.createTermElement({
               id: parseInt(term.id),
               name: term.name,
               hasChildren: term.hasChildren,
               path: term.path || null,
               show: showPath
            });
         });
         p.appendChild(btn);
      }
            if (element) {
               this.activeStore.storeDOMElement(term.id, 'list-item', element);
               this.ui.termsList.appendChild(element);
            }
         }
      console.error('Store fetch error:', error);
   }
   async batchFetchTaxonomies() {
      if (this.batchFetch.size === 0) return;
      const taxonomies = Array.from(this.batchFetch);
      this.batchFetch.clear();
      try {
         await this.store.setFilters({
            taxonomy: taxonomies.join(','),
            page: 1,
            search: '',
            parent: 0
         });
      } catch (error) {
         console.error('Failed to batch fetch taxonomies:', error);
      }
   }
   preloadTaxonomy(taxonomy) {
      this.store.setFilters( {
         taxonomy: taxonomy,
         page: 1,
         search: '',
         parent: 0
      });
   }
   /**
    * Create individual term element
    */
   createTermElement(termData) {
      if (!termData || !termData.name) return null;
   /**************************************************
    LOADING
   **************************************************/
   setCreateButton(field, show = true) {
      if (!field.canCreate || !this.creator) return;
      const listItem = window.getTemplate('termListItem').cloneNode(true);
      listItem.dataset.id = termData.id;
      const conf = (this.container.open) ? this.ui : field.ui;
      if (!conf.create?.button || !conf.create?.span) return;
      const isSelected = this.selectedTerms.has(termData.id);
      const checkbox = listItem.querySelector('input');
      const label = listItem.querySelector('label');
      const nameSpan = listItem.querySelector('span, .term-name');
      const createButton = conf.create.button;
      createButton.hidden = !show;
      const buttonSpan = conf.create.span;
      const input = (this.container.open) ? conf.search.input : conf.search;
      if (!input) return;
      if (checkbox && label && nameSpan) {
         checkbox.id = `${this.currentConfig.container.id}${termData.id}`;
         checkbox.name = `${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`;
         checkbox.value = termData.id;
         checkbox.disabled = !isSelected && this.disabled;
         checkbox.checked = isSelected;
      let results = this.currentTerms()??[];
      let matches = results.map(t => t.name);
         label.htmlFor = checkbox.id;
         label.title = termData.path || termData.name;
         label.dataset.path = termData.path;
      let query = input.value;
      const willShow = show && query.length >= 2 && !matches.includes(query);
      createButton.hidden = !willShow;
      if (willShow) {
         buttonSpan.textContent = input.value??'';
      }
   }
   async maybeCreateTerm(e) {
      const field = this.currentField();
      if (!field) return;
         nameSpan.textContent = termData.show ? termData.path : termData.name;
      window.debouncer.cancel(`${field.id}-search-results`);
      let data = {
         taxonomy: field.taxonomy,
         parent: this.store.filters.parent??0
      }
      if (termData.hasChildren) {
         const childrenToggle = window.getTemplate ?
            window.getTemplate('termChildrenToggle') :
            this.createChildrenToggle();
      if (!this.container.open || this.ui.search.input.value !== '') {
         data.name = (this.container.open) ? this.ui.search.input.value : field.ui.search.value;
      } else {
         data.parent = this.creator.ui.parent.value??data.parent;
         data.name = this.creator.ui.name.value??false;
      }
         if (childrenToggle) {
            childrenToggle.ariaLabel = `View sub-terms of ${termData.name}`;
            listItem.appendChild(childrenToggle);
      if (data.parent !== undefined && data.name) {
         this.setMessage(field,true, `Creating "${data.name}"...`);
         this.setCreateButton(field,false);
         if (this.container.open) {
            window.removeChildren(this.ui.terms.list);
         } else {
            field.ui.search.disabled = true;
            if (field.ui.dropdown.wrapper) {
               field.ui.dropdown.wrapper.hidden = false;
            }
         }
         let term = await this.creator.handleTermCreation(data);
         if (term) {
            // Stop any typeLoop animation and show success message WITHOUT typeLoop
            this.setMessage(field,true, `"${term.name}" created!`, false);
            this.addSelected(term.id, field.id);
            this.updateFieldValue(field.id);
            // For autocomplete, show the newly created term in dropdown
            if (!this.container.open && field.ui.dropdown.list) {
               window.removeChildren(field.ui.dropdown.list);
               const termElement = this.createAutocompleteTerm(term);
               if (termElement) {
                  termElement.classList.add('newly-created');
                  field.ui.dropdown.list.append(termElement);
               }
            }
            this.scheduleHideDropdown(field.id, 300);
            this.setMessage(field,false);
         } else {
            // Creation failed - hide immediately
            this.setMessage(field,false);
            if (!this.container.open && field.ui.dropdown.wrapper) {
               field.ui.dropdown.wrapper.hidden = true;
            }
         }
         if (!this.container.open) {
            field.ui.search.disabled = false;
            field.ui.search.value = '';
         }
      }
      return listItem;
   }
   setMessage(field, show = true, message = '', type = true) {
      const conf = this.container.open||field.isFilter ? this.ui : (field.isFilter ? null : field.ui);
      if (!conf?.message?.message) return;
   /**
    * Create children toggle button
    */
   createChildrenToggle() {
      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'toggle-children';
      button.innerHTML = '→';
      return button;
   }
      message = (message === '') ? `No ${field.plural??'items'} found.` : message;
   /**
    * Update breadcrumb navigation
    */
   updateBreadcrumbs(termId, termName) {
      // This is a simplified version - you'd want to maintain a proper breadcrumb trail
      const breadcrumb = window.getTemplate('termBreadcrumb').cloneNode(true);
      breadcrumb.dataset.id = termId;
      breadcrumb.textContent = termName;
      breadcrumb.title = termName;
      const p = conf.message.message;
      const pText = conf.message.text;
      // Remove any existing breadcrumbs after this level
      const existingCrumb = this.ui.breadcrumbs.nav.querySelector(`[data-id="${termId}"]`);
      if (existingCrumb) {
         // Remove all breadcrumbs after this one
         while (existingCrumb.nextElementSibling) {
            existingCrumb.nextElementSibling.remove();
      p.hidden = !show;
      if (show) {
         if (message && pText) {
            if (type && window.typeLoop && pText) {
               if (this.messageText[field.id]) {
                  this.messageText[field.id]();
                  delete this.messageText[field.id];
               }
               this.messageText[field.id] = window.typeLoop(pText, message);
            } else {
               pText.textContent = message;
            }
         }
      } else {
         this.ui.breadcrumbs.nav.appendChild(breadcrumb);
         if (this.messageText[field.id]) {
            this.messageText[field.id]();
            delete this.messageText[field.id];
         }
      }
   }
   /**
    * Show loading state
    */
   showLoading() {
      this.ui.loading.loading.hidden = false;
      this.modal.classList.add('loading');
      const searchQuery = this.activeStore?.filters?.search || '';
      const currentParent = this.activeStore?.filters?.parent || 0;
      let message = searchQuery !== '' ?
         `searching for "${searchQuery}" items` :
         currentParent === 0 ?
            'loading items' :
            `loading child items`;
      if (window.typeLoop) {
         this.stopTyping = window.typeLoop(this.ui.loading.text, message);
      } else {
         this.ui.loading.text.textContent = message;
      }
   /**************************************************
    SUBSCRIBERS
   **************************************************/
   subscribe(callback) {
      this.subscribers.add(callback);
      return () => this.subscribers.delete(callback);
   }
   /**
    * Hide loading state
    */
   hideLoading() {
      this.ui.loading.loading.hidden = true;
      this.modal.classList.remove('loading');
      if (this.stopTyping) {
         this.stopTyping();
      }
   notify(event, data={}) {
      this.subscribers.forEach(callback => {
         try {
            callback(event, data);
         } catch (error) {
            console.error('Subscriber error:', error);
         }
      });
   }
   /**
    * Show empty state message
    */
   showEmptyState(message = 'No items found.') {
      const emptyElement = window.getTemplate('noResults').cloneNode(true);
      if (message && emptyElement.querySelector('span')) {
         emptyElement.querySelector('span').textContent = message;
      }
      this.ui.termsList.appendChild(emptyElement);
   }
   /**
    * Get field ID from any element within the field
    */
   getFieldId(element) {
      if (element.dataset.fieldId) {
         return element.dataset.fieldId;
      }
      const fieldContainer = element.closest('[data-field-id]');
      if (fieldContainer) {
         return fieldContainer.dataset.fieldId;
      }
      return null;
   }
   /**
    * Clean up
    */
   /******************************************************
    CLEANUP
   ******************************************************/
   destroy() {
      // Remove event listeners
      document.removeEventListener('click', this.handleClick);
      document.removeEventListener('change', this.handleChange);
      // Cancel all debounced operations for this instance
      this.fields.forEach((field, fieldId) => {
         window.debouncer.cancel(`${fieldId}-search`);
         window.debouncer.cancel(`${fieldId}-search-results`);
      });
      // Clear intervals and cleanup
      // Stop any typeLoop animations
      Object.keys(this.messageText).forEach(key => {
         if (this.messageText[key]) {
            this.messageText[key]();
         }
      });
      this.messageText = {};
      // Disconnect observer
      if (this.ui.terms?.sentinel) {
         this.observer?.unobserve(this.ui.terms.sentinel);
      }
      this.observer?.disconnect();
      this.lazyObserver?.disconnect();
      // Unsubscribe from all stores
      this.storeSubscriptions.forEach(unsubscribe => unsubscribe());
      // Remove event listeners
      document.removeEventListener('click', this.clickHandler);
      document.removeEventListener('change', this.changeHandler);
      document.removeEventListener('input', this.inputHandler);
      document.removeEventListener('focus', this.focusHandler, true);
      document.removeEventListener('blur', this.blurHandler, true);
      // Destroy all stores
      this.stores.forEach(store => store.destroy());
      // Clear all maps
      // Clear data structures
      this.subscribers.clear();
      this.fields.clear();
      this.stores.clear();
      this.storeSubscriptions.clear();
      this.selectedTerms.clear();
      this.batchFetch.clear();
      // Cleanup creator if exists
      if (this.creator) {
         this.creator.destroy();
         this.creator = null;
      }
      // Unsubscribe from store
      if (this.store) {
         this.store = null;
      }
   }
}
/**
 * Initialize singleton
 */
document.addEventListener('DOMContentLoaded', function() {
   if (!window.jvbSelector) {
      window.jvbSelector = new TaxonomySelector();
   }
   window.auth.subscribe((event) => {
      if (event === 'auth-loaded') {
         window.jvbSelector = new TaxonomySelector();
      }
   });
});