Jake Vanderwerf
2026-01-05 9f86429a1252b45c95b7c62fbaa1b82de3723997
assets/js/concise/TaxonomySelector.js
@@ -1,19 +1,40 @@
/**
 * TaxonomySelector - Streamlined version
 * Manages taxonomy selection fields with DataStore integration
 */
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;
      this.isInitializing = true;
      this.taxonomiesToFetch = new Set();
      this.subscribers = new Set();
      this.fields = new Map();
      this.selectedTerms = new Map();  // a map of fieldId => Set of selected term Ids
      this.loadedTaxonomies = new Set(); // a set of taxonomies, to know whether we should preload a newly registered field
      this.batchFetch = new Set();
      // Register DataStore
      const store = window.jvbStore.register('taxonomies', {
      this.activeField = null;
      this.isInitializing = true;
      this.init();
   }
   init() {
      this.initStore();
      this.initElements();
      this.initModal();
      this.scanExistingFields();
      this.initListeners();
      if (this.needsCreator() && window.jvbTaxCreator) {
         this.creator = new window.jvbTaxCreator(this);
      }
      this.isInitializing = false
      this.batchFetchTaxonomies().then(()=> {});
   }
   initStore() {
      const store = window.jvbStore.register(
         'taxonomies',
         {
         storeName: 'terms',
         keyPath: 'id',
         showLoading: false,
@@ -33,469 +54,200 @@
         },
         required: 'taxonomy',
         delayFetch: true,
      });
         }
      );
      this.store = store.terms;
      // Field management
      this.fields = new Map();
      this.selectedTerms = new Map(); // Current modal selection
      // Modal context
      this.activeField = null;
      this.currentConfig = null;
      this.disabled = false;
      // Search contexts
      this.searchContexts = new Map();
      this.init();
   }
   init() {
      this.initModal();
      this.scanExistingFields();
      this.initGlobalListeners();
      // Initialize creator if needed
      if (this.needsCreator() && window.jvbTaxCreator) {
         this.creator = new window.jvbTaxCreator(this);
      }
      this.store.subscribe(this.handleStoreEvent.bind(this));
      this.isInitializing = false;
      this.batchFetchTaxonomies();
   }
   needsCreator() {
      return Array.from(this.fields.values()).some(field =>
         field.canCreate || field.hasAutocomplete
      );
   }
   /***********************************************************************
    * DATASTORE EVENT HANDLING
    ***********************************************************************/
   handleStoreEvent(event, data) {
      const handlers = {
         'data-loaded': () => this.handleDataLoaded(data),
         'filters-changed': () => this.handleFiltersChanged(data),
         'fetch-error': () => this.handleFetchError(data.error),
      };
      handlers[event]?.();
   }
   handleDataLoaded(data) {
      const taxonomy = this.store.filters.taxonomy;
      // Update field states for affected taxonomies
      if (taxonomy) {
         const taxonomies = taxonomy.includes(',')
            ? taxonomy.split(',').map(t => t.trim())
            : [taxonomy];
         taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
      }
      // Initialize displays on first load
      if (this.isInitializing) {
         this.fields.forEach((config, fieldId) => {
            if (config.selectedTerms.size > 0) {
               this.initFieldDisplay(fieldId);
            }
         });
      }
      // Render based on context
      this.renderSearchResults(data);
   }
   renderSearchResults(data) {
      const context = this.getActiveSearchContext();
      if (context === 'modal') {
         this.renderModalResults(data);
      } else if (context === 'autocomplete') {
         this.renderAutocompleteResults(data);
      }
   }
   getActiveSearchContext() {
      if (this.modal?.open) return 'modal';
      if (this.activeField && this.searchContexts.has(this.activeField)) {
         return this.searchContexts.get(this.activeField);
      }
      return null;
   }
   renderModalResults(data) {
      this.hideLoading();
      const terms = this.store.getFiltered();
      const response = this.store.lastResponse?.page || {};
      const isSearch = data.filters?.search?.length > 0;
      const append = response.page > 1;
      this.notify('terms-loaded', { terms, filters: data.filters });
      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);
         if (response.has_more) {
            this.observer.observe(this.ui.sentinel);
         } else {
            this.observer.unobserve(this.ui.sentinel);
         }
      }
      this.a11y?.announce(terms.length, append);
   }
   renderAutocompleteResults(data) {
      const field = this.fields.get(this.activeField);
      if (!field?.autocompleteDropdown) return;
      const terms = this.store.getFiltered();
      const query = data.filters?.search || '';
      this.showAutocompleteResults(field, terms, query);
      this.searchContexts.delete(this.activeField);
   }
   handleFiltersChanged(data) {
      if (this.modal?.open) {
         this.showLoading();
      }
   }
   handleFetchError(error) {
      this.hideLoading();
      const context = this.getActiveSearchContext();
      if (context === 'autocomplete') {
         this.showAutocompleteError(this.activeField);
         this.searchContexts.delete(this.activeField);
      } else {
         this.handleError(error, 'fetch');
      }
   }
   /***********************************************************************
    * FIELD MANAGEMENT
    ***********************************************************************/
   updateFieldsForTaxonomy(taxonomy) {
      this.getFieldsForTaxonomy(taxonomy).forEach(field => {
         this.updateFieldButtonState(field.id);
      });
   }
   updateFieldButtonState(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field) return;
      const hasTerms = Array.from(this.store.data.values())
         .some(term => term.taxonomy === field.taxonomy);
      if (field.toggle) {
         field.toggle.disabled = !hasTerms && !field.canCreate;
         field.toggle.title = !hasTerms
            ? `No ${this.getLabel(field.taxonomy, 'single')} available`
            : `Select ${this.getLabel(field.taxonomy, 'plural')}`;
      }
   }
   getFieldsForTaxonomy(taxonomy) {
      return Array.from(this.fields.values())
         .filter(field => field.taxonomy === taxonomy);
   }
   scanExistingFields(container = document.body) {
      container.querySelectorAll('.field.taxonomy, .field.post').forEach(selector => {
         try {
            this.registerField(selector);
         } catch (error) {
            this.handleError(error, 'scanExistingFields', selector.dataset.name);
         }
      });
   }
   registerField(field) {
      const input = field.querySelector('input[type=hidden]');
      if (!input) return false;
      const fieldId = this.createFieldId(field);
      field.dataset.fieldId = fieldId;
      const button = field.querySelector('button.taxonomy-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,
         hasAutocomplete: 'autocomplete' in button.dataset,
         autocompleteDropdown: field.querySelector('.autocomplete-dropdown') || null,
         canCreate: 'creatable' in button.dataset,
         isRequired: 'required' in button.dataset,
         selectedTerms: new Set(),
         toggle: button,
         selectedContainer: field.querySelector('.selected-items'),
      };
      // Parse initial values
      const value = input.value.trim();
      if (value) {
         value.split(',')
            .map(id => parseInt(id.trim()))
            .filter(id => !isNaN(id))
            .forEach(id => config.selectedTerms.add(id));
      }
      this.fields.set(fieldId, config);
      // Queue for batch fetch
      if (this.isInitializing) {
         this.taxonomiesToFetch.add(config.taxonomy);
      }
      // Initialize display
      if (config.selectedTerms.size > 0) {
         this.initFieldDisplay(fieldId);
      }
      return fieldId;
   }
   createFieldId(field) {
      this.index++;
      return 'selector-' + this.index;
   }
   async initFieldDisplay(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field || field.selectedTerms.size === 0) return;
      Array.from(field.selectedTerms).forEach(termId => {
         const term = this.store.get(termId);
         if (term) {
            this.addTermDisplay(termId, term.name, term.path, 'field', fieldId);
         }
      });
   }
   /***********************************************************************
    * MODAL INITIALIZATION
    ***********************************************************************/
   initModal() {
      this.modal = document.querySelector('dialog#jvb-selector');
      if (!this.modal) {
         console.warn('Taxonomy selector modal not found');
         return;
      }
      this.initModalElements();
      this.modalInstance = new window.jvbModal(this.modal, {
         handleForm: false
      });
      this.modalInstance.subscribe((event) => {
         if (event === 'modal-open') this.openModal();
         if (event === 'modal-close') this.closeModal();
      });
   }
   initModalElements() {
      const selectors = {
   /******************************************************************
    ELEMENTS
    ******************************************************************/
   initElements() {
      this.selectors = {
         search: {
            input: '[type=search]',
            container: '.search-wrapper'
            clear: '.clear-search',
            container: '.search-wrapper',
            results: '.search-results'
         },
         termsList: '.items-container',
         termsWrap: '.items-wrap',
         breadcrumbs: {
         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',
         },
         loading: {
            loading: '.loading',
            text: '.loading span'
            text: '.loading span',
         },
         selectedTerms: '.selected-items',
         sentinel: '.scroll-sentinel',
         selected: '.selected-items',
         modal: {
            title: '#modal-title',
            content: '.modal-content',
            count: '.selection-count'
         },
         create: {
            details: '.create-new-term',
            summary: '.create-new-term summary',
            label: {
               name: '[for=term_name]',
               parent: '[for=select_parent]'
         favourites: '.favourite-terms',
         field: {
            toggle: 'button.taxonomy-toggle',
            value: 'input[type="hidden"]',
            selected: '.selected-items',
            dropdown: '.search-results',
            search: '[data-autocomplete]',
            }
         }
      };
      this.ui = window.uiFromSelectors(selectors);
      this.ui = window.uiFromSelectors(this.selectors);
   }
      // Initialize infinite scroll observer
   initListeners() {
      this.observer = new IntersectionObserver((entries) => {
         entries.forEach(entry => {
            if (entry.isIntersecting) {
               this.loadMoreTerms();
               this.nextPage();
            }
         });
      }, {
         root: this.ui.termsWrap,
         root: this.ui.terms.sentinel,
         threshold: 0.5
      });
   }
   /***********************************************************************
    * GLOBAL EVENT LISTENERS
    ***********************************************************************/
      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);
   initGlobalListeners() {
      document.addEventListener('click', this.handleClick.bind(this));
      document.addEventListener('change', this.handleChange.bind(this));
      document.addEventListener('input', this.handleInput.bind(this));
      document.addEventListener('focus', this.handleFocus.bind(this), true);
      document.addEventListener('blur', this.handleBlur.bind(this), true);
      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);
   }
   handleClick(e) {
      // Toggle button
      if (window.targetCheck(e, '.taxonomy-toggle')) {
      const fieldId = this.getFieldId(e.target);
      const field = this.fields.get(fieldId);
      if (!fieldId || !field) return;
      const autoComplete = window.targetCheck(e, '[data-autocomplete-select]');
      if (autoComplete) {
         let termId = parseInt(autoComplete.dataset.id);
         this.addSelected(termId, fieldId);
         if (field.ui.dropdown) {
            field.ui.dropdown.hidden = true;
         }
         if (field.ui.search) {
            field.ui.search.value = '';
         }
      }
      const toggleButton = window.targetCheck(e, field.ui.toggle);
      if (toggleButton) {
         e.preventDefault();
         const fieldId = this.getFieldId(e.target);
         const field = this.fields.get(fieldId);
         if (field) this.setActiveField(fieldId, true);
         this.openModal(fieldId);
         return;
      }
      // Remove selected term
      const removeButton = window.targetCheck(e, 'button.remove-item');
      if (removeButton && e.target.closest('.jvb-selector')) {
      if (removeButton) {
         const fieldId = this.getFieldId(removeButton);
         const termId = removeButton.closest('.selected-item').dataset.id;
         this.removeSelectedTerm(fieldId, termId);
         const termId = removeButton.closest('.selected-item').dataset.id??false;
         if (fieldId && termId) {
            this.removeSelected(termId, fieldId);
         }
         return;
      }
      // Modal close
      if (e.target.matches('.modal-close')) {
         this.modalInstance?.handleClose();
         this.modal?.handleClose();
         return;
      }
      // Modal clicks
      if (this.modal?.contains(e.target)) {
         this.handleModalClick(e);
      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);
      }
      const dropdown = window.targetCheck(e, field.selectors.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) {
      // Hidden input changes
      const taxonomyField = window.targetCheck(e, '.taxonomy.field, .post.field');
      if (taxonomyField && e.target.type === 'hidden') {
         const fieldId = this.getFieldId(e.target);
         this.updateFieldFromInput(fieldId);
      if (!this.container.contains(e.target)) {
         return;
      }
      if (e.target.type !== 'checkbox') return;
      e.preventDefault();
      e.stopPropagation();
      // Modal checkboxes
      if (this.modal?.contains(e.target)) {
         this.handleModalChange(e);
      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) {
      // Modal search
      if (this.modal?.contains(e.target) && e.target.type === 'search') {
         this.performSearch(e.target.value.trim(), 'modal');
         return;
      }
      // Autocomplete
      if ('autocomplete' in e.target.dataset) {
         const fieldId = this.getFieldId(e.target);
      let fieldId = this.getFieldId(e.target)??this.activeField;
      if (!fieldId) return;
         const field = this.fields.get(fieldId);
         if (field?.hasAutocomplete) {
            this.performSearch(e.target.value.trim(), 'autocomplete', fieldId);
         }
      }
   }
   handleFocus(e) {
      if (!('autocomplete' in e.target.dataset)) return;
      const fieldId = this.getFieldId(e.target);
      const field = this.fields.get(fieldId);
      if (field?.hasAutocomplete) {
         this.preloadTaxonomy(field.taxonomy);
      }
   }
   handleBlur(e) {
      if (!('autocomplete' in e.target.dataset)) return;
      setTimeout(() => {
         const fieldId = this.getFieldId(e.target);
         const field = this.fields.get(fieldId);
         if (field?.autocompleteDropdown) {
            field.autocompleteDropdown.hidden = true;
         }
         this.searchContexts.delete(fieldId);
      }, 200);
   }
   /***********************************************************************
    * UNIFIED SEARCH
    ***********************************************************************/
   performSearch(query, context = 'modal', fieldId = null) {
      const field = context === 'autocomplete'
         ? this.fields.get(fieldId)
         : this.currentConfig;
      if (!field) return;
      // Autocomplete validation
      if (context === 'autocomplete') {
         field.currentAutocompleteQuery = query;
         if (query.length < 2) {
            if (field.autocompleteDropdown) {
               field.autocompleteDropdown.hidden = true;
            }
            return;
         }
         this.searchContexts.set(fieldId, 'autocomplete');
      if (!this.container.open) {
         this.activeField = fieldId;
         if (field.autocompleteDropdown) {
            field.autocompleteDropdown.hidden = false;
         }
      }
      // Debounced search
      const query = e.target.value.trim();
      window.debouncer.schedule(
         `taxonomy-search-${context}-${fieldId || 'modal'}`,
         `${fieldId}-search`,
         async () => {
            await this.store.setFilters({
               taxonomy: field.taxonomy,
@@ -503,507 +255,537 @@
               page: 1,
               parent: query ? 0 : (this.store.filters.parent || 0)
            });
            if (context === 'modal') {
               window.removeChildren(this.ui.termsList);
            if (this.container.open) {
               window.removeChildren(this.ui.terms.list);
            }
         },
         300
         100
      );
   }
   /***********************************************************************
    * MODAL OPERATIONS
    ***********************************************************************/
   handleFocus(e) {
      const fieldId = this.getFieldId(e.target);
      const field = this.fields.get(fieldId);
      if (!fieldId || !field) return;
      if (!field.hasAutocomplete && !field.hasSearch) return;
   setActiveField(fieldId, openModal = false) {
      window.debouncer.cancel(`${fieldId}-search-results`);
      if (!this.container.open){
      this.activeField = fieldId;
      this.currentConfig = this.fields.get(fieldId);
      if (openModal) {
         this.modalInstance.handleOpen();
      }
      this.store.setFilter('taxonomy', this.currentConfig.taxonomy);
      // Reset modal selection state
      this.selectedTerms.clear();
      // Copy field selections to modal
      this.currentConfig.selectedTerms.forEach(termId => {
         const term = this.store.get(termId);
         if (term) {
            this.selectedTerms.set(termId, {
               id: termId,
               name: term.name,
               path: term.path
            });
         }
      });
   }
   handleModalClick(e) {
      if (window.targetCheck(e, '.remove-item')) {
         const 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')) {
         const termItem = e.target.closest('li');
         this.navigateToChild(
            parseInt(termItem.dataset.id),
            termItem.querySelector('.term-name').textContent
         );
      } else if (window.targetCheck(e, '.path-level')) {
         const pathLevel = window.targetCheck(e, '.path-level');
         this.navigateToPath(parseInt(pathLevel.dataset.id) || 0);
         this.preloadTaxonomy(field.taxonomy);
      }
   }
   handleModalChange(e) {
      if (e.target.type !== 'checkbox') return;
   //Hide autocomplete dropdown on blur
   handleBlur(e) {
      const fieldId = this.getFieldId(e.target);
      const field = this.fields.get(fieldId);
      if (!fieldId || ! field) return;
      if (!field.hasAutocomplete) return;
      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);
      }
      this.scheduleHideDropdown(fieldId);
   }
   openModal() {
      if (!this.currentConfig) {
         console.error('No active field set');
         return;
      }
   scheduleHideDropdown(fieldId){
      const field = this.fields.get(fieldId);
      if (!field) return;
      this.updateModalUI();
      this.updateModalSelections();
      window.removeChildren(this.ui.termsList);
      this.showLoading();
   }
   closeModal() {
      this.observer.unobserve(this.ui.sentinel);
      window.removeChildren(this.ui.termsList);
      this.notify('selected-terms', {
         terms: this.selectedTerms,
         taxonomy: this.currentConfig.taxonomy
      });
      if (this.activeField) {
         this.saveSelectionsToField(this.activeField);
      }
      window.debouncer.schedule(
         `${fieldId}-search-results`,
         () => {
      this.activeField = null;
      this.currentConfig = null;
            field.ui.dropdown.hidden = true;
         },
         1500
      );
   }
   updateModalUI() {
      const singular = this.getLabel(this.currentConfig.taxonomy, 'single');
      const plural = this.getLabel(this.currentConfig.taxonomy, 'plural');
   /******************************************************************
    MODAL
    ******************************************************************/
   initModal() {
      this.modalID = 'dialog#jvb-selector';
      this.container = document.querySelector(this.modalID);
      this.ui.modal.title.textContent = `Select ${plural}`;
      this.modal = new window.jvbModal(
         this.container,
         {
            handleForm: false,
            save: null,
            open: null
         }
      );
      this.modal.subscribe((event, data) => {
         switch (event) {
         }
      });
   }
   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.activeField = fieldId;
      this.ui.modal.title.textContent = `Select ${field.plural}`;
      if (this.ui.search.container) {
         this.ui.search.container.style.display = this.currentConfig.canSearch ? 'block' : 'none';
         this.ui.search.container.hidden = !field.canSearch;
      }
      if (this.ui.create.details) {
         this.ui.create.details.style.display = this.currentConfig.canCreate ? 'block' : 'none';
         this.ui.create.details.hidden = !this.currentConfig.canCreate;
         this.ui.create.details.hidden = !field.canCreate;
         if (this.ui.create.summary) {
            this.ui.create.summary.textContent = `Add new ${singular}`;
            this.ui.create.summary.textContent = `Add new ${field.singular}`;
         }
         if (this.ui.create.label.name) {
            this.ui.create.label.name.textContent = `Name this ${singular}`;
            this.ui.create.label.name.textContent = `Name this ${field.singular}`;
         }
         if (this.ui.create.label.parent) {
            this.ui.create.label.parent.textContent = `Nest it under`;
         }
      }
      let message = `Opened ${field.singular} selection. Choose from checkboxes, or search to filter results.`;
      this.a11y?.announce(`Opened ${singular} selection. Choose from checkboxes or search to filter results.`);
   }
      window.removeChildren(this.ui.terms.list);
      this.modal.handleOpen();
      this.setLoading();
   updateModalSelections() {
      window.removeChildren(this.ui.selectedTerms);
      this.selectedTerms.forEach((termData, id) => {
         this.addTermDisplay(id, termData.name, termData.path, 'modal');
      this.store.setFilters({
         taxonomy: field.taxonomy,
         page: 1,
         search: '',
         parent: 0,
      });
      this.checkSelectionLimits();
      this.a11y.announce(message);
   }
   addSelectedTermToModal(id, name, path) {
      this.selectedTerms.set(id, { id, name, path });
      this.addTermDisplay(id, name, path, 'modal');
      this.checkSelectionLimits();
      const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
      if (checkbox) checkbox.checked = true;
   }
   removeSelectedTermFromModal(id) {
      this.selectedTerms.delete(parseInt(id));
      const selectedItem = this.ui.selectedTerms.querySelector(`[data-id="${id}"]`);
      if (selectedItem) selectedItem.remove();
      const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
      if (checkbox) checkbox.checked = false;
      this.checkSelectionLimits();
   }
   checkSelectionLimits() {
      if (!this.currentConfig || this.currentConfig.maxSelection === 0) {
         return;
      }
      this.disabled = this.selectedTerms.size >= this.currentConfig.maxSelection;
      this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
         if (!checkbox.checked) {
            checkbox.disabled = this.disabled;
         }
      });
   }
   saveSelectionsToField(fieldId) {
      const field = this.fields.get(fieldId);
   closeModal() {
      this.modal.handleClose();
      const field = this.fields.get(this.activeField);
      if (!field) return;
      this.observer.unobserve(this.ui.terms.sentinel);
      window.removeChildren(this.ui.terms.list);
      field.selectedTerms.clear();
      window.removeChildren(field.selectedContainer);
      this.selectedTerms.forEach((termData, id) => {
         field.selectedTerms.add(id);
         this.addTermDisplay(id, termData.name, termData.path, 'field', fieldId);
      this.notify('selected-terms', {
         terms: this.selectedTerms.get(this.activeField),
         taxonomy: field.taxonomy
      });
      field.input.value = Array.from(field.selectedTerms).join(',');
      field.input.dispatchEvent(new Event('change', { bubbles: true }));
      this.activeField = null;
      let message = `Closed ${field.singular} selector.`;
      this.a11y.announce(message);
   }
   /***********************************************************************
    * TERM DISPLAY
    ***********************************************************************/
   navigateToParent() {
      const current = this.store.filters.parent;
      if (current === 0) return;
      let term = this.store.get(parseInt(current));
      if (!term) 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);
   }
   addTermDisplay(termId, termName, termPath, context = 'field', fieldId = null) {
      const config = context === 'field'
         ? this.fields.get(fieldId)
         : this.currentConfig;
   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});
   }
      const container = context === 'field'
         ? config.selectedContainer
         : this.ui.selectedTerms;
      if (container.querySelector(`[data-id="${termId}"]`)) return;
   addTermToModal(termId) {
      const term = this.store.get(termId);
      if (!term) return;
      const item = window.getTemplate('selectedTerm');
      item.dataset.id = termId;
      item.dataset.path = termPath;
      item.dataset.name = termName;
      item.dataset.taxonomy = config.taxonomy;
      item.querySelector('.item-name').textContent = termPath;
      item.querySelector('button').title = `Remove ${termName}`;
      item.querySelector('span').textContent = term.path;
      item.querySelector('button').title = `Remove ${name}`;
      container.appendChild(item);
      if (context === 'modal') {
         const checkbox = this.ui.termsList.querySelector(`input[value="${termId}"]`);
         if (checkbox) checkbox.checked = true;
      this.ui.selected.append(item);
   }
   /******************************************************************
    FIELDS
    ******************************************************************/
   scanExistingFields(container = document.body) {
      container.querySelectorAll('[data-type="selector"]').forEach(
         selector => {
            try {
               this.registerField(selector);
            } catch (error) {
               this.error.log(error, {
                  component: 'TaxonomySelector',
                  action: 'scanExistingFields',
                  container: selector.dataset.name
               });
      }
   }
   removeSelectedTerm(fieldId, termId) {
      const field = this.fields.get(fieldId);
      if (!field) return;
      field.selectedTerms.delete(parseInt(termId));
      const selectedItem = field.selectedContainer.querySelector(`[data-id="${termId}"]`);
      if (selectedItem) selectedItem.remove();
      field.input.value = Array.from(field.selectedTerms).join(',');
      field.input.dispatchEvent(new Event('change', { bubbles: true }));
      );
   }
   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) {
         value.split(',')
            .map(id => parseInt(id.trim()))
            .filter(id => !isNaN(id))
            .forEach(id => field.selectedTerms.add(id));
         this.initFieldDisplay(fieldId);
      }
   }
   /***********************************************************************
    * NAVIGATION
    ***********************************************************************/
   navigateToParent() {
      this.store.setFilters({ parent: 0, page: 1 });
      window.removeChildren(this.ui.termsList);
      this.ui.breadcrumbs.back.hidden = true;
   }
   navigateToChild(termId, termName) {
      this.store.setFilters({ parent: termId, page: 1 });
      window.removeChildren(this.ui.termsList);
      this.updateBreadcrumbs(termId, termName);
      this.ui.breadcrumbs.back.hidden = false;
   }
   navigateToPath(parentId) {
      this.store.setFilters({ parent: parentId, page: 1 });
      window.removeChildren(this.ui.termsList);
      this.ui.breadcrumbs.back.hidden = parentId === 0;
   }
   loadMoreTerms() {
      const currentPage = this.store.filters.page || 1;
      this.store.setFilter('page', currentPage + 1);
   }
   updateBreadcrumbs(termId, termName) {
      const breadcrumb = window.getTemplate('termBreadcrumb');
      breadcrumb.dataset.id = termId;
      breadcrumb.textContent = termName;
      breadcrumb.title = termName;
      const existingCrumb = this.ui.breadcrumbs.nav.querySelector(`[data-id="${termId}"]`);
      if (existingCrumb) {
         while (existingCrumb.nextElementSibling) {
            existingCrumb.nextElementSibling.remove();
         }
      } else {
         this.ui.breadcrumbs.nav.appendChild(breadcrumb);
      }
   }
   /***********************************************************************
    * RENDERING
    ***********************************************************************/
   renderTerms(terms = null, append = false, showPath = false) {
      if (!terms) terms = this.store.getFiltered();
      if (!append) window.removeChildren(this.ui.termsList);
      if (terms.length === 0) {
         if (!append) this.showEmptyState();
   registerField(element, options = {}) {
      let input = element.querySelector('input[type="hidden"]');
      if (!input) {
         console.warn('TaxonomySelector: No hidden input found for field', element);
         return;
      }
      const currentParent = this.store.filters.parent || 0;
      this.ui.breadcrumbs.back.hidden = currentParent === 0;
      if (!('fieldId' in element.dataset)) {
         element.dataset.fieldId = window.generateID('selector');
      }
      const fieldId = element.dataset.fieldId;
      let selectors = this.selectors.field;
      let button = element.querySelector('button.taxonomy-toggle');
      if (options.size === 0){
         if (!button) return;
         options = button.dataset;
         if (options.size === 0) return;
      } else if (Object.hasOwn(options, 'toggle')) {
         button = document.querySelector(options.toggle);
         selectors.toggle = options.toggle;
      }
      const config = {
         id: fieldId,
         value: input,
         element: element,
         taxonomy: options.taxonomy??false,
         singular: options.single??'',
         plural: options.plural??'',
         name: element.dataset.field,
         canSearch: Object.hasOwn(options, 'search'),
         limit: options.limit??0,
         hasAutocomplete: Object.hasOwn(options, 'autocomplete'),
         canCreate: Object.hasOwn(options, 'creatable'),
         isRequired: Object.hasOwn(options, 'required'),
         toggle: button,
         selectors: selectors,
         ui: window.uiFromSelectors(selectors, element),
         checked: false,
      };
      if (!config.taxonomy) return;
      this.fields.set(fieldId, config);
      //Check for stored selected terms in hidden input
      let selected = new Set();
      input.value.value.trim()
         .split(',')
         .map(id => parseInt(id.trim()))
         .filter(id => !isNaN(id))
         .forEach(id => selected.add(id));
      this.selectedTerms.set(fieldId, selected);
      if (this.isInitializing) {
         this.batchFetch.add(config.taxonomy);
      }
      this.updateFieldUI(fieldId);
      return fieldId;
   }
   addSelected(termId, fieldId = null) {
      if (!fieldId) fieldId = this.activeField;
      const field = this.fields.get(fieldId);
      const term = this.store.get(termId);
      if (!field || !term) return;
      const selected = this.selectedTerms.get(fieldId);
      if (field.limit !== 0 && selected.size >= field.limit) return;
      selected.add(parseInt(termId));
      this.addTermToDisplay(termId, fieldId);
      this.updateFieldValue(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));
      const selectedItem = field.ui.selected.querySelector(`[data-i"${termId}"]`);
      if (selectedItem) selectedItem.remove();
      if (this.container.open) {
         let item = this.ui.selected.querySelector(`[data-id="${termId}"]`);
         if (item) item.remove();
      }
      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));
      field.ui.value = selected.join(',');
   }
   checkLimits(fieldId) {
      if (!this.container.open) return;
      const field = this.fields.get(fieldId);
      if (!field || field.limit === 0) return;
      const disabled = this.selectedTerms.get(fieldId).size >= field.limit;
      this.setCheckboxes(disabled);
   }
   updateFieldUI(fieldId) {
      const field = this.fields.get(fieldId);
      let selected = this.selectedTerms.get(fieldId);
      if (!field || 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.checked && field.taxonomy === taxonomy);
      const hasItems = Array.from(this.store.data.values())
         .some(term=>term.taxonomy === taxonomy);
      fields.forEach(field => {
         field.ui.toggle.disabled = !hasItems && !field.canCreate;
         field.ui.toggle.title = !hasItems
            ? `No ${field.singular} available`
            : `Select ${field.plural}`;
         field.checked = true;
      });
   }
   showModalTerms(append = true, showPath = false) {
      const terms = this.store.getFiltered();
      if (terms.size === 0) return;
      if (!append) {
         window.removeChildren(this.ui.terms.list);
      }
      const currentParent = this.store.filters.parent??0;
      this.ui.nav.back.hidden = currentParent === 0;
      const fragment = document.createDocumentFragment();
      terms.forEach(term => {
         const element = this.createTermElement({
            id: parseInt(term.id),
            name: term.name,
            hasChildren: term.hasChildren,
            path: term.path || null,
            show: showPath
            show: showPath,
            ... term
         });
         if (element) {
            fragment.appendChild(element);
         }
         });
         if (element) fragment.appendChild(element);
      });
      this.ui.terms.list.append(fragment);
   }
   createTermElement(term) {
      if (!term || !term.name) return null;
      this.ui.termsList.appendChild(fragment);
      const item = window.getTemplate('termListItem');
      item.dataset.id = term.id;
      const isSelected = this.selectedTerms.get(this.activeField).has(term.id);
      let [
         checkbox,
         label,
         nameSpan
      ] = [
         item.querySelector('input'),
         item.querySelector('label'),
         item.querySelector('span, .term-name')
      ];
      let field = this.currentField();
      let limitReached = field.limit > 0 && this.selectedTerms.get(this.activeField).size >= field.limit;
      if (checkbox && label && nameSpan) {
         [
            checkbox.id,
            checkbox.name,
            checkbox.value,
            checkbox.disabled,
            checkbox.checked,
            label.htmlFor,
            label.title,
            label.dataset.path,
            nameSpan.textContent
         ] = [
            `${field.element.id}-${term.id}`,
            `${field.container.id}-${field.taxonomy}-select`,
            term.id,
            !isSelected && limitReached,
            isSelected,
            `${field.element.id}-${term.id}`,
            term.path??term.name,
            term.path,
            term.show ? term.path : term.name
         ];
         if (term.hasChildren) {
            const toggle = window.getTemplate('termChildrenToggle');
            if (toggle) {
               toggle.ariaLabel = `View ${field.plural} nested under ${term.name}`;
               item.append(toggle);
            }
         }
   }
   createTermElement(termData) {
      if (!termData?.name) return null;
      const listItem = window.getTemplate('termListItem');
      listItem.dataset.id = termData.id;
      const isSelected = this.selectedTerms.has(termData.id);
      const checkbox = listItem.querySelector('input');
      const label = listItem.querySelector('label');
      const nameSpan = listItem.querySelector('.term-name');
      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;
      label.htmlFor = checkbox.id;
      label.title = termData.path || termData.name;
      label.dataset.path = termData.path;
      nameSpan.textContent = termData.show ? termData.path : termData.name;
      if (termData.hasChildren) {
         const childrenToggle = window.getTemplate('termChildrenToggle');
         childrenToggle.ariaLabel = `View sub-terms of ${termData.name}`;
         listItem.appendChild(childrenToggle);
      return item;
      }
      return listItem;
   }
   showAutocompleteTerms() {
      const field = this.currentField();
      const terms = this.currentTerms();
      if (!field || terms.size ===0) return;
   /***********************************************************************
    * AUTOCOMPLETE
    ***********************************************************************/
   showAutocompleteResults(field, terms, query) {
      if (!field?.autocompleteDropdown) return;
      const dropdown = field.autocompleteDropdown;
      const dropdown = field.ui.dropdown;
      window.removeChildren(dropdown);
      if (terms.length === 0) {
         this.showEmptyState('No items found.', dropdown);
         this.showEmptyState(`No ${field.plural} found.`, dropdown);
      } else {
         const fragment = document.createDocumentFragment();
         terms.forEach(term => {
            const item = this.createAutocompleteItem(field, term);
            if (item) fragment.appendChild(item);
         });
         dropdown.appendChild(fragment);
            const item = this.createAutocompleteTerm(term);
            if (item) {
               dropdown.append(item);
            }
         })
      }
      // Create button if allowed and no exact match
      const currentQuery = field.currentAutocompleteQuery || query;
      if (field.canCreate && currentQuery) {
         const exactMatch = terms.find(term =>
            term.name.toLowerCase() === currentQuery.toLowerCase()
         );
         if (!exactMatch) {
            dropdown.appendChild(this.createAutocompleteCreateButton(currentQuery));
      const query = field.ui.search?.value;
      if (field.canCreate && query.length >= 2 && this.creator) {
         const createButton = this.createTermButton(query);
         if (createButton) {
            dropdown.append(createButton);
         }
      }
      dropdown.hidden = false;
   }
   createAutocompleteTerm(term) {
      const item = window.getTemplate('autocompleteItem');
      if (!item) return;
   createAutocompleteItem(field, term) {
      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'autocomplete-item';
      button.dataset.id = term.id;
      button.dataset.name = term.name;
      button.dataset.path = term.path || term.name;
      button.textContent = term.path || term.name;
      button.addEventListener('click', () => {
         field.selectedTerms.add(parseInt(term.id));
         this.addTermDisplay(term.id, term.name, term.path, 'field', field.id);
         field.input.value = Array.from(field.selectedTerms).join(',');
         field.input.dispatchEvent(new Event('change', { bubbles: true }));
         field.autocompleteDropdown.hidden = true;
         const input = field.container.querySelector('input[data-autocomplete]');
         if (input) input.value = '';
      });
      return button;
      item.dataset.id = term.id;
      item.textContent = term.path || term.name;
      return item;
   }
   createAutocompleteCreateButton(query) {
      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'autocomplete-item create-term';
      button.dataset.query = query;
      const strong = document.createElement('strong');
      strong.textContent = 'Create: ';
      button.appendChild(strong);
      button.appendChild(document.createTextNode(`"${query}"`));
      return button;
   }
   showAutocompleteError(fieldId) {
   /******************************************************************
    UI
    ******************************************************************/
   addTermToDisplay(termId, fieldId) {
      const term = this.store.get(termId);
      const field = this.fields.get(fieldId);
      if (!field?.autocompleteDropdown) return;
      if (!term || !field) return;
      //if the term already exists in the selected items, bail early
      if (field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
      window.removeChildren(field.autocompleteDropdown);
      this.showEmptyState('Hmmm... something went wrong', field.autocompleteDropdown);
      const item = window.getTemplate('selectedTerm');
      if (!item) return;
      item.dataset.id = termId;
      item.dataset.taxonomy = field.taxonomy;
      item.querySelector('.item-name').textContent = term.path;
      item.querySelector('button').title = `Remove ${term.name}`;
      field.ui.selected.append(item);
      if (this.container.open) {
         this.addTermToModal(termId);
         const checkbox = this.ui.terms.list.querySelector(`input[value="${termId}"]`);
         if (checkbox) checkbox.checked = true;
      }
   }
   createTermButton(query) {
      const button = window.getTemplate('autocompleteButton');
      if(!button) return;
      let queryEl = button.querySelector('span');
      queryEl.textContent = `"${query}"`;
      return button;
   }
   /***********************************************************************
    * UI STATES
    ***********************************************************************/
   updateBreadcrumbs(termId) {
      const nav = this.ui.nav.nav;
      if (!nav) return;
      const existingCrumb = Array.from(nav.children)
         .find(crumb => parseInt(crumb.dataset.id) === termId);
   showLoading() {
      this.ui.loading.loading.hidden = false;
      this.modal.classList.add('loading');
      const searchQuery = this.store.filters.search || '';
      const currentParent = this.store.filters.parent || 0;
      const 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);
      if (existingCrumb) {
         // Remove all siblings after this crumb
         let nextSibling = existingCrumb.nextElementSibling;
         while (nextSibling) {
            const toRemove = nextSibling;
            nextSibling = nextSibling.nextElementSibling;
            toRemove.remove();
         }
      } else {
         this.ui.loading.text.textContent = message;
         // Add new breadcrumb
         const term = this.store.get(termId);
         if (!term) return;
         const crumb = window.getTemplate('termBreadcrumb');
         if (!crumb) return;
         crumb.dataset.id = termId;
         crumb.textContent = term.name;
         crumb.title = term.name;
         nav.append(crumb);
      }
   }
   hideLoading() {
      this.ui.loading.loading.hidden = true;
      this.modal.classList.remove('loading');
   updateSelectionCount() {
      if (!this.container.open) return;
      const field = this.fields.get(this.activeField);
      if (!field) return;
      if (this.stopTyping) {
         this.stopTyping();
      }
      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`;
   }
   showEmptyState(message = 'No items found.', container = null) {
      if (!container) container = this.ui.termsList;
      const emptyElement = window.getTemplate('noResults');
      const messageSpan = emptyElement.querySelector('span');
      if (message && messageSpan) {
         messageSpan.textContent = message;
      }
      container.appendChild(emptyElement);
   /******************************************************************
    UTILITY
    ******************************************************************/
   currentField() {
      return this.fields.get(this.activeField)??false;
   }
   /***********************************************************************
    * UTILITIES
    ***********************************************************************/
   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;
@@ -1012,54 +794,169 @@
      return fieldContainer?.dataset.fieldId || null;
   }
   getLabel(taxonomy, type = 'single') {
      return jvbSettings.labels[taxonomy]?.[type] || taxonomy;
   /**
    * Sets all checkbox disabled (or not)
    * @param {Boolean} disabled
    */
   setCheckboxes(disabled) {
      this.ui.terms.list.querySelectorAll('input[type=checkbox]').forEach(checkbox => {
         if (!checkbox.checked) {
            checkbox.disabled = disabled;
         }
      });
   }
   /******************************************************************
    DATASTORE HELPERS
   ******************************************************************/
   handleStoreEvent(event, data) {
      const handlers = {
         'data-loaded': () => this.handleDataLoaded(),
         'filters-changed': () => this.handleFiltersChanged(),
         'fetch-error': () => this.handleFetchError()
      };
      handlers[event]?.();
   }
   handleDataLoaded() {
      const taxonomy = this.store.filters.taxonomy;
      if (taxonomy?.includes(',')) {
         const taxonomies = taxonomy.split(',').map(t => t.trim());
         taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
      }
      if (this.container.open) {
         this.showResults();
         return;
      }
      if (this.activeField) {
         this.showResults(true);
      }
   }
   showResults(isAutoComplete = false) {
      this.setLoading(false);
      const terms = this.store.getFiltered();
      const filters = this.store.filters;
      const response = this.store.lastResponse?.page || {};
      const isSearch = filters.search && filters.search.length > 0;
      const append = filters.page > 1;
      const field = this.currentField();
      this.notify('terms-loaded', {
         terms,
         filters
      });
      if (terms.length === 0) {
         if (!append) {
            this.showEmptyState(isSearch ? `No matching ${field.plural}.` : `No ${field.plural} available.`);
         }
         this.observer.unobserve(this.ui.terms.sentinel);
      } else {
         if (!isAutoComplete) {
            this.showModalTerms(append, isSearch);
            if (response.has_more) {
               this.observer.observe(this.ui.terms.sentinel);
            } else {
               this.observer.unobserve(this.ui.terms.sentinel);
            }
         } else {
            this.showAutocompleteTerms()
         }
      }
      this.a11y.announce(terms.length, append);
   }
   handleFiltersChanged() {
      // if (this.modal?.open) {
      //    this.setLoading();
      // }
   }
   handleFetchError(error) {
      this.setLoading(false);
   }
   async batchFetchTaxonomies() {
      if (this.taxonomiesToFetch.size === 0) return;
      if (this.batchFetch.size === 0) return;
      const taxonomies = Array.from(this.taxonomiesToFetch);
      this.taxonomiesToFetch.clear();
      const taxonomies = Array.from(this.batchFetch);
      taxonomies.forEach(tax => this.loadedTaxonomies.add(tax));
      this.batchFetch.clear();
      this.store.setFilters({
      try {
         taxonomies.forEach(tax => this.loadedTaxonomies.add(tax));
         await this.store.setFilters({
         taxonomy: taxonomies.join(','),
         page: 1,
         search: '',
         parent: 0
      });
      } catch (error) {
         console.error('Failed to batch fetch taxonomies:', error);
      }
   }
   async preloadTaxonomy(taxonomy) {
      await this.store.setFilters({
   preloadTaxonomy(taxonomy) {
      if (this.loadedTaxonomies.has(taxonomy)) return;
      this.store.setFilters( {
         taxonomy: taxonomy,
         page: 1,
         search: '',
         parent: 0
      });
      this.loadedTaxonomies.add(taxonomy);
   }
   handleError(error, context, detail = null) {
      console.error(`Taxonomy ${context} error:`, error, detail);
   /**************************************************
    LOADING
   **************************************************/
   setLoading(on = true) {
      this.ui.loading.loading.hidden = on;
      this.modal.classList.toggle('loading', on);
      if (this.error?.log) {
         this.error.log(error, {
            component: 'TaxonomySelector',
            action: context,
            detail: detail
         });
      if (on) {
         let searchQuery = this.store.filters.search || '';
         searchQuery = searchQuery === '' ? false : searchQuery;
         const currentParent = this.store.filters.parent || 0;
         const message = searchQuery
            ? `Searching for "${searchQuery} items` :
            currentParent === 0
               ? 'loading items'
               : 'loading child items';
         if (window.typeLoop && this.ui.loading.text) {
            this.stopTyping = window.typeLoop(this.ui.loading.text, message);
         } else {
            this.ui.loading.text.textContenet = message;
      }
      if (this.modal?.open) {
         this.showEmptyState('Error loading. Please try again.');
      } else {
         if (this.stopTyping) {
            this.stopTyping();
            this.stopTyping = null;
      }
   }
   }
   showEmptyState(message = 'No items found.', container = null) {
      if (!container) container = this.ui.terms.list;
      const emptyElement = window.getTemplate('noTermResults');
      const span = emptyElement.querySelector('span');
      if (message && span) {
         span.textContent = message;
      }
      container.append(emptyElement);
   }
   /**************************************************
    SUBSCRIBERS
   **************************************************/
   subscribe(callback) {
      this.subscribers.add(callback);
      return () => this.subscribers.delete(callback);
   }
   notify(event, data = {}) {
      this.subscribers.forEach(callback => {
         try {
@@ -1069,25 +966,24 @@
         }
      });
   }
   /******************************************************
    CLEANUP
   ******************************************************/
   destroy() {
      document.removeEventListener('click', this.handleClick);
      document.removeEventListener('change', this.handleChange);
      document.removeEventListener('input', this.handleInput);
      document.removeEventListener('focus', this.handleFocus);
      document.removeEventListener('blur', this.handleBlur);
      document.removeEventListener('click', this.clickHandler);
      document.removeEventListener('change', this.changeHandler);
      document.removeEventListener('input', this.inputHandler);
      document.removeEventListener('focus', this.focusHandler);
      document.removeEventListener('blur', this.blurHandler);
      this.observer?.disconnect();
      this.store.destroy();
      this.subscribers.clear();
      this.fields.clear();
      this.selectedTerms.clear();
      this.searchContexts.clear();
   }
}
// Initialize on auth ready
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', function() {
   window.auth.subscribe((event) => {
      if (event === 'auth-loaded') {
         window.jvbSelector = new TaxonomySelector();