Jake Vanderwerf
2026-01-06 2c955cebb5f1e01fbdb866b50d296fe9fbd852b8
assets/js/concise/TaxonomySelector.js
@@ -9,11 +9,11 @@
      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();
      this.activeField = null;
      this.isInitializing = true;
      this.messageText = {}
      this.init();
   }
@@ -70,7 +70,11 @@
            input: '[type=search]',
            clear: '.clear-search',
            container: '.search-wrapper',
            results: '.search-results'
            results: '.search-results',
         },
         create: {
            button: 'button.submit-term',
            span: '.submit-term span',
         },
         terms: {
            list: '.items-container',
@@ -83,9 +87,9 @@
            child: '.toggle-children',
            pathLevel: '.path-level',
         },
         loading: {
            loading: '.loading',
            text: '.loading span',
         message: {
            message: 'p.message',
            text: 'p.message span',
         },
         selected: '.selected-items',
         modal: {
@@ -98,12 +102,23 @@
            toggle: 'button.taxonomy-toggle',
            value: 'input[type="hidden"]',
            selected: '.selected-items',
            dropdown: '.search-results',
            search: '[data-autocomplete]',
            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.ui = window.uiFromSelectors(this.selectors, this.container);
   }
   initListeners() {
@@ -132,7 +147,7 @@
   }
   handleClick(e) {
      const fieldId = this.getFieldId(e.target);
      const fieldId = (this.container.open) ? this.activeField : this.getFieldId(e.target);
      const field = this.fields.get(fieldId);
      if (!fieldId || !field) return;
@@ -140,8 +155,8 @@
      if (autoComplete) {
         let termId = parseInt(autoComplete.dataset.id);
         this.addSelected(termId, fieldId);
         if (field.ui.dropdown) {
            field.ui.dropdown.hidden = true;
         if (field.ui.dropdown.wrapper) {
            field.ui.dropdown.wrapper.hidden = true;
         }
         if (field.ui.search) {
@@ -149,24 +164,25 @@
         }
      }
      const toggleButton = window.targetCheck(e, field.ui.toggle);
      const toggleButton = window.targetCheck(e, this.selectors.field.toggle);
      if (toggleButton) {
         e.preventDefault();
         this.openModal(fieldId);
         return;
      }
      const removeButton = window.targetCheck(e, 'button.remove-item');
      const removeButton = window.targetCheck(e, '.remove-term');
      if (removeButton) {
         const fieldId = this.getFieldId(removeButton);
         const termId = removeButton.closest('.selected-item').dataset.id??false;
         const termId = removeButton.closest('[data-id]').dataset.id??false;
         if (fieldId && termId) {
            this.removeSelected(termId, fieldId);
            this.removeSelected(parseInt(termId), fieldId);
         }
         return;
      }
      if (e.target.matches('.modal-close')) {
         this.updateFieldValue(fieldId);
         this.modal?.handleClose();
         return;
      }
@@ -194,11 +210,10 @@
         this.navigateTo(termId);
      }
      const dropdown = window.targetCheck(e, field.selectors.dropdown);
      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);
@@ -217,6 +232,13 @@
         }
      }
      if (this.creator) {
         let button = window.targetCheck(e, this.selectors.create.button);
         if (button) {
            this.maybeCreateTerm(e).then(()=>{});
         }
      }
   }
   handleChange(e) {
      if (!this.container.contains(e.target)) {
@@ -240,29 +262,59 @@
      if (!fieldId) return;
      const field = this.fields.get(fieldId);
      if (!field) return;
      if (e.target.type === 'checkbox') return;
      e.preventDefault();
      e.stopPropagation();
      //If it's the autocomplete field, we need to set the active field
      if (!this.container.open) {
         this.activeField = fieldId;
         this.setField(fieldId);
      }
      const query = e.target.value.trim();
      let query = e.target.value.trim();
      this.setMessage(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)
            });
            if (this.container.open) {
               window.removeChildren(this.ui.terms.list);
            }
         },
         100
      );
   }
   setField(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field) {
         console.error('No field found...');
         return;
      }
      this.activeField = fieldId;
      this.setMessage(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) {
      const fieldId = this.getFieldId(e.target);
      const field = this.fields.get(fieldId);
@@ -272,8 +324,7 @@
      window.debouncer.cancel(`${fieldId}-search-results`);
      if (!this.container.open){
         this.activeField = fieldId;
         this.preloadTaxonomy(field.taxonomy);
         this.setField(fieldId);
      }
   }
@@ -282,7 +333,7 @@
      const fieldId = this.getFieldId(e.target);
      const field = this.fields.get(fieldId);
      if (!fieldId || ! field) return;
      if (!field.hasAutocomplete) return;
      if (!field.hasAutocomplete || this.container.open) return;
      this.scheduleHideDropdown(fieldId);
   }
@@ -294,8 +345,12 @@
      window.debouncer.schedule(
         `${fieldId}-search-results`,
         () => {
            this.activeField = null;
            field.ui.dropdown.hidden = true;
            if (!this.container.open) {
               this.activeField = null;
            }
            if (field.ui.dropdown.wrapper) {
               field.ui.dropdown.wrapper.hidden = true;
            }
         },
         1500
      );
@@ -312,7 +367,6 @@
         this.container,
         {
            handleForm: false,
            save: null,
            open: null
         }
      );
@@ -338,36 +392,18 @@
      const field = this.fields.get(fieldId);
      if (!field) return;
      this.activeField = fieldId;
      this.setField(fieldId);
      this.ui.modal.title.textContent = `Select ${field.plural}`;
      if (this.ui.search.container) {
         this.ui.search.container.hidden = !field.canSearch;
      }
      if (this.ui.create.details) {
         this.ui.create.details.hidden = !field.canCreate;
         if (this.ui.create.summary) {
            this.ui.create.summary.textContent = `Add new ${field.singular}`;
         }
         if (this.ui.create.label.name) {
            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`;
         }
      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.terms.list);
      this.modal.handleOpen();
      this.setLoading();
      this.store.setFilters({
         taxonomy: field.taxonomy,
         page: 1,
         search: '',
         parent: 0,
      });
      this.a11y.announce(message);
   }
@@ -394,7 +430,10 @@
      const current = this.store.filters.parent;
      if (current === 0) return;
      let term = this.store.get(parseInt(current));
      if (!term) return;
      if (!term) {
         this.navigateTo(0);
         return;
      }
      let parent = term.parent;
      this.navigateTo(parseInt(parent));
   }
@@ -419,11 +458,17 @@
   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;
      const item = window.getTemplate('selectedTerm');
      if (!item) return;
      item.dataset.id = termId;
      item.querySelector('span').textContent = term.path;
      item.querySelector('button').title = `Remove ${name}`;
      item.dataset.taxonomy = field.taxonomy;
      item.querySelector('.item-name').textContent = term.path;
      item.querySelector('button').title = `Remove ${term.name}`;
      this.ui.selected.append(item);
   }
@@ -461,10 +506,18 @@
      let selectors = this.selectors.field;
      let button = element.querySelector('button.taxonomy-toggle');
      if (options.size === 0){
      if (Object.keys(options).length === 0){
         if (!button) return;
         options = button.dataset;
         if (options.size === 0) 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')
         };
         if (Object.keys(options).length === 0) return;
      } else if (Object.hasOwn(options, 'toggle')) {
         button = document.querySelector(options.toggle);
         selectors.toggle = options.toggle;
@@ -478,21 +531,33 @@
         singular: options.single??'',
         plural: options.plural??'',
         name: element.dataset.field,
         canSearch: Object.hasOwn(options, 'search'),
         canSearch: options.search??false,
         limit: options.limit??0,
         hasAutocomplete: Object.hasOwn(options, 'autocomplete'),
         canCreate: Object.hasOwn(options, 'creatable'),
         isRequired: Object.hasOwn(options, 'required'),
         hasAutocomplete: options.autocomplete??false,
         canCreate: options.creatable??false,
         isRequired: options.required??false,
         toggle: button,
         create: {
            button: null,
            span: null
         },
         selectors: selectors,
         ui: window.uiFromSelectors(selectors, element),
         checked: false,
      };
      if (!config.taxonomy) return;
      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);
      //Check for stored selected terms in hidden input
      this.setSelectedFromValue(input);
      this.setSelectedFromValue(fieldId, input);
      if (this.isInitializing) {
@@ -504,8 +569,13 @@
   }
   setSelectedFromValue(fieldId, input) {
      if (!input) return;
      if (!fieldId) return;
      let field = this.fields.get(fieldId);
      if (!field) return;
      let selected = new Set();
      input.value.value.trim()
      input.value.trim()
         .split(',')
         .map(id => parseInt(id.trim()))
         .filter(id => !isNaN(id))
@@ -524,8 +594,10 @@
      if (field.limit !== 0 && selected.size >= field.limit) return;
      selected.add(parseInt(termId));
      if (!this.container.open) {
         this.updateFieldValue(fieldId);
      }
      this.addTermToDisplay(termId, fieldId);
      this.updateFieldValue(fieldId);
      this.checkLimits(fieldId);
   }
   removeSelected(termId, fieldId = null) {
@@ -535,20 +607,27 @@
      if (!field || !term) return;
      this.selectedTerms.get(fieldId).delete(parseInt(termId));
      const selectedItem = field.ui.selected.querySelector(`[data-i"${termId}"]`);
      const selectedItem = field.ui.selected.querySelector(`[data-id="${termId}"]`);
      if (selectedItem) selectedItem.remove();
      if (this.container.open) {
         let item = this.ui.selected.querySelector(`[data-id="${termId}"]`);
         if (item) item.remove();
         let checkbox = this.ui.terms.list.querySelector(`[type=checkbox][data-id="${termId}"]`);
         if (checkbox) {
            checkbox.checked = false;
         }
      }
      this.updateFieldValue(fieldId);
      if (!this.container.open) {
         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(',');
      field.ui.value.value = selected.join(',');
   }
   checkLimits(fieldId) {
@@ -561,8 +640,9 @@
   updateFieldFromInput(input) {
      const fieldId = this.getFieldId(input);
      if (!fieldId) return;
      const field = this.fields.get(fieldId);
      if (!fieldId || !field) return;
      if(!field) return;
      this.setSelectedFromValue(fieldId, input);
      this.updateFieldUI(fieldId);
@@ -570,7 +650,7 @@
   updateFieldUI(fieldId) {
      const field = this.fields.get(fieldId);
      let selected = this.selectedTerms.get(fieldId);
      let selected = this.selectedTerms.get(fieldId)??new Set();
      if (!field || selected.size === 0) return;
      Array.from(selected).forEach(termId => {
@@ -594,11 +674,28 @@
      });
   }
   showModalTerms(append = true, showPath = false) {
   showModalTerms(showPath = false) {
      const field = this.currentField();
      const terms = this.store.getFiltered();
      if (terms.size === 0) return;
      if (!append) {
         window.removeChildren(this.ui.terms.list);
      if (terms.length === 0) {
         if (this.store.filters.page??1 === 1) {
            window.removeChildren(this.ui.terms.list);
         }
         this.setMessage(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);
         }
      }
      if (this.ui.terms.sentinel) {
         if (this.store.lastResponse?.has_more) {
            this.observer.observe(this.ui.terms.sentinel);
         } else {
            this.observer.unobserve(this.ui.terms.sentinel);
         }
      }
      const currentParent = this.store.filters.parent??0;
@@ -611,10 +708,12 @@
            ... term
         });
         if (element) {
            fragment.appendChild(element);
            fragment.append(element);
         }
      });
      this.setMessage(false);
      this.ui.terms.list.append(fragment);
   }
   createTermElement(term) {
@@ -638,6 +737,7 @@
      let limitReached = field.limit > 0 && this.selectedTerms.get(this.activeField).size >= field.limit;
      if (checkbox && label && nameSpan) {
         [
            checkbox.dataset.id,
            checkbox.id,
            checkbox.name,
            checkbox.value,
@@ -648,8 +748,9 @@
            label.dataset.path,
            nameSpan.textContent
         ] = [
            term.id,
            `${field.element.id}-${term.id}`,
            `${field.container.id}-${field.taxonomy}-select`,
            `${field.element.id}-${field.taxonomy}-select`,
            term.id,
            !isSelected && limitReached,
            isSelected,
@@ -673,30 +774,28 @@
   showAutocompleteTerms() {
      const field = this.currentField();
      const terms = this.currentTerms();
      if (!field || terms.size ===0) return;
      if (!field) return;
      const dropdown = field.ui.dropdown;
      const dropdown = field.ui.dropdown.list;
      if (!dropdown) return;
      window.removeChildren(dropdown);
      if (terms.length === 0) {
         this.showEmptyState(`No ${field.plural} found.`, dropdown);
         this.setMessage(true, `No ${field.plural} found.`, false);
      } else {
         terms.forEach(term => {
            const item = this.createAutocompleteTerm(term);
            if (item) {
               dropdown.append(item);
            }
         })
         });
         this.setMessage(false);
      }
      this.setCreateButton(true);
      const query = field.ui.search?.value;
      if (field.canCreate && query.length >= 2 && this.creator) {
         const createButton = this.createTermButton(query);
         if (createButton) {
            dropdown.append(createButton);
         }
      if (field.ui.dropdown?.wrapper) {
         field.ui.dropdown.wrapper.hidden = false;
      }
      dropdown.hidden = false;
   }
   createAutocompleteTerm(term) {
      const item = window.getTemplate('autocompleteItem');
@@ -732,15 +831,6 @@
         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;
   }
   updateBreadcrumbs(termId) {
      const nav = this.ui.nav.nav;
@@ -826,11 +916,16 @@
   handleStoreEvent(event, data) {
      const handlers = {
         'data-loaded': () => this.handleDataLoaded(),
         'filters-changed': () => this.handleFiltersChanged(),
         'filters-changed': () => this.handleFiltersChanged(data),
         'fetch-error': () => this.handleFetchError()
      };
      handlers[event]?.();
      try {
         handlers[event]?.(data);
      } catch (error) {
         console.error(`Error handling store event "${event}":`, error);
         this.setMessage(true, 'An error occurred loading data', false);
      }
   }
   handleDataLoaded() {
      const taxonomy = this.store.filters.taxonomy;
@@ -845,63 +940,52 @@
      }
      if (this.activeField) {
         this.showResults(true);
         return;
      }
      this.setMessage(false);
   }
   showResults(isAutoComplete = false) {
      this.setLoading(false);
      this.setMessage(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()
         }
      if (isAutoComplete) {
         this.showAutocompleteTerms();
      } else {
         this.showModalTerms(isSearch);
      }
      this.a11y.announce(terms.length, append);
      this.a11y.announce(terms.length);
   }
   handleFiltersChanged() {
      // if (this.modal?.open) {
      //    this.setLoading();
      // }
   handleFiltersChanged(data) {
      //maybe do something?
   }
   handleFetchError(error) {
      this.setLoading(false);
      const field = this.currentField();
      const message = field
         ? `Failed to load ${field.plural}`
         : 'Failed to load data';
      this.setMessage(true, message, false);
      console.error('Store fetch error:', error);
   }
   async batchFetchTaxonomies() {
      if (this.batchFetch.size === 0) return;
      const taxonomies = Array.from(this.batchFetch);
      taxonomies.forEach(tax => this.loadedTaxonomies.add(tax));
      this.batchFetch.clear();
      try {
         taxonomies.forEach(tax => this.loadedTaxonomies.add(tax));
         await this.store.setFilters({
            taxonomy: taxonomies.join(','),
            page: 1,
@@ -914,55 +998,110 @@
   }
   preloadTaxonomy(taxonomy) {
      if (this.loadedTaxonomies.has(taxonomy)) return;
      this.store.setFilters( {
         taxonomy: taxonomy,
         page: 1,
         search: '',
         parent: 0
      });
      this.loadedTaxonomies.add(taxonomy);
   }
   /**************************************************
    LOADING
   **************************************************/
   setLoading(on = true) {
      this.ui.loading.loading.hidden = on;
      this.modal.classList.toggle('loading', on);
   setCreateButton(show = true) {
      const field = this.currentField();
      if (!field || !field.canCreate || !this.creator) return;
      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';
      const conf = (this.container.open) ? this.ui : field.ui;
         if (window.typeLoop && this.ui.loading.text) {
            this.stopTyping = window.typeLoop(this.ui.loading.text, message);
         } else {
            this.ui.loading.text.textContenet = message;
         }
      } else {
         if (this.stopTyping) {
            this.stopTyping();
            this.stopTyping = null;
         }
      if (!conf.create?.button || !conf.create?.span) return;
      const createButton = conf.create.button;
      const buttonSpan = conf.create.span;
      const input = (this.container.open) ? conf.search.input : conf.search;
      if (!input) return;
      let results = this.currentTerms()??[];
      let matches = results.map(t => t.name);
      let query = input.value;
      const willShow = show && query.length >= 2 && !matches.includes(query);
      createButton.hidden = !willShow;
      if (willShow) {
         buttonSpan.textContent = input.value??'';
      }
   }
   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;
   async maybeCreateTerm(e) {
      const field = this.currentField();
      if (!field) return;
      window.debouncer.cancel(`${field.id}-search-results`);
      let data = {
         taxonomy: field.taxonomy,
         parent: this.store.filters.parent??0
      }
      container.append(emptyElement);
      //If it's autocomplete or the selector's search input, we just need the name
      if (!this.container.open || this.ui.search.input.value !== '') {
         data.name = (this.container.open) ? this.ui.search.input.value : field.ui.search.value;
      } else {
         //Otherwise, we've created it from the details element
         data.parent = this.creator.ui.parent.value??data.parent;
         data.name = this.creator.ui.name.value??false;
      }
      if (data.parent !== undefined && data.name) {
         this.setMessage(true, `Creating "${data.name}"...`);
         this.setCreateButton(false);
         if (this.container.open) {
            window.removeChildren(this.ui.terms.list);
         } else {
            field.ui.search.disabled = true;
            window.removeChildren(field.ui.dropdown.list);
            if (field.ui.dropdown.wrapper) {
               field.ui.dropdown.wrapper.hidden = false;
            }
         }
         let term = await this.creator.handleTermCreation(data);
         if (term) {
            this.addSelected(term.id, field.id);
         }
         if (!this.container.open) {
            field.ui.search.disabled = false;
            field.ui.search.value = '';
         }
         this.scheduleHideDropdown(field.id);
         this.setMessage(false);
      }
   }
   setMessage(show = true, message = '', type = true) {
      const field = this.currentField();
      if (!field) return;
      message = (message === '') ? `No ${field.plural??'items'} found.` : message;
      const conf = (this.container.open) ? this.ui : field.ui;
      const p = conf.message.message;
      const pText = conf.message.text;
      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 {
         if (this.messageText[field.id]) {
            this.messageText[field.id]();
            delete this.messageText[field.id];
         }
      }
   }
   /**************************************************
    SUBSCRIBERS
@@ -984,16 +1123,49 @@
    CLEANUP
   ******************************************************/
   destroy() {
      // Cancel all debounced operations for this instance
      this.fields.forEach((field, fieldId) => {
         window.debouncer.cancel(`${fieldId}-search`);
         window.debouncer.cancel(`${fieldId}-search-results`);
      });
      // 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();
      // Remove event listeners
      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);
      document.removeEventListener('focus', this.focusHandler, true);
      document.removeEventListener('blur', this.blurHandler, true);
      this.observer?.disconnect();
      // Clear data structures
      this.subscribers.clear();
      this.fields.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;
      }
   }
}
@@ -1004,3 +1176,4 @@
      }
   });
});