/** * Centralized Taxonomy Selector * Handles all taxonomy selection fields on the page from a single location */ class TaxonomySelector { constructor() { this.a11y = window.jvbA11y; // this.cache = window.jvbCache; this.error = window.jvbError; this.index = -1; this.stores = new Map(); // Central field management this.fields = new Map(); // fieldId -> field configuration this.terms = new Map(); // taxonomy -> Map(termId -> term data) this.selectedTerms = new Map(); // Current selection (cleared on modal close) // Current modal context this.activeField = null; this.currentConfig = null; // Modal state this.page = 1; this.hasMore = true; this.isLoading = false; this.searchQuery = ''; this.navigationPath = []; this.currentParent = 0; this.currentParentName = ''; this.fetchSpecificTerms = false; this.disabled = false; // Search debouncing this.searchHandler = null; // Prefetch system this.prefetchCache = new Map(); this.prefetchQueue = []; this.isPrefetching = false; this.init(); } /** * Initialize the selector */ init() { this.initModal(); this.scanExistingFields(); this.initGlobalListeners(); this.initPrefetching(); } /** * 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 }); } }); } /** * Register a taxonomy field */ registerField(field, options = {}) { let input = field.querySelector('input[type=hidden]'); if (!input) { return; } if (!('fieldId' in field.dataset)) { field.dataset.fieldId = this.createFieldId(field); } let fieldId = field.dataset.fieldId; let button = field.querySelector('button.taxonomy-toggle'); let config = { id: fieldId, input: input, container: field, taxonomy: button.dataset.taxonomy, name: field.dataset.field, singular: button.dataset.singular, plural: button.dataset.plural, maxSelection: parseInt(button.dataset.max) || 0, canSearch: 'search' in button.dataset, canCreate: 'creatable' in button.dataset, isRequired: 'required' in button.dataset, selectedTerms: new Set(input.value.split(',')), toggle: button, selectedContainer: field.querySelector('.selected-items'), ...options }; this.fields.set(fieldId, config); // Initialize display for any pre-selected values this.initFieldDisplay(fieldId); return fieldId; } /** * Create unique field ID */ createFieldId(field) { let input = field.querySelector('input[type=hidden]'); this.index++; return 'selector-'+this.index; } /** * Initialize display for a field with existing values */ initFieldDisplay(fieldId) { const field = this.fields.get(fieldId); if (!field) return; const value = field.input.value.trim(); if (value !== '') { const selectedIds = value.split(',') .map(id => parseInt(id.trim())) .filter(id => !isNaN(id)); if (selectedIds.length > 0) { this.updateFieldDisplay(fieldId, selectedIds); } } } /** * Update field display with selected term IDs */ async updateFieldDisplay(fieldId, selectedIds) { const field = this.fields.get(fieldId); if (!field || selectedIds.length === 0) return; // Check if we already have these terms cached const taxonomy = field.taxonomy; const needsFetch = []; const cachedTerms = []; selectedIds.forEach(termId => { if (this.terms.has(taxonomy) && this.terms.get(taxonomy).has(termId)) { const term = this.terms.get(taxonomy).get(termId); cachedTerms.push(term); field.selectedTerms.add(termId); } 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 { this.fetchSpecificTerms = needsFetch.join(','); const fetchedTerms = await this.fetchTerms(fieldId); fetchedTerms.forEach(term => { field.selectedTerms.add(term.id); this.addTermToDisplay(fieldId, term.id, term.name, term.path); }); } catch (error) { console.error('Failed to fetch missing terms:', error); } finally { this.fetchSpecificTerms = false; } } } /** * 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, onOpen: () => this.openModal(), onClose: () => this.closeModal() }); } /** * Initialize modal element references */ initModalElements() { this.elements = { searchInput: this.modal.querySelector('input[type=search]'), termsList: this.modal.querySelector('.items-container'), termsWrap: this.modal.querySelector('.items-wrap'), breadcrumbs: this.modal.querySelector('nav.term-navigation'), loading: this.modal.querySelector('.loading'), loadingText: this.modal.querySelector('.loading span'), clearSearch: this.modal.querySelector('.clear-search'), selectedTerms: this.modal.querySelector('.selected-items'), backButton: this.modal.querySelector('.back-to-parent'), sentinel: this.modal.querySelector('.scroll-sentinel'), modalTitle: this.modal.querySelector('#modal-title'), modalContent: this.modal.querySelector('.modal-content'), createNewSection: this.modal.querySelector('.create-new-term'), favouriteTerms: this.modal.querySelector('.favourite-terms'), }; // Initialize intersection observer for infinite scroll this.observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting && !this.isLoading && this.hasMore) { this.fetchTerms(this.activeField); } }); }, { root: this.elements.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); // Clear modal selection state this.selectedTerms.clear(); // Copy field's current selections to modal state if (this.currentConfig.selectedTerms) { this.currentConfig.selectedTerms.forEach(termId => { // Find term data to populate modal selection const taxonomy = this.currentConfig.taxonomy; if (this.terms.has(taxonomy) && this.terms.get(taxonomy).has(termId)) { const term = this.terms.get(taxonomy).get(termId); this.selectedTerms.set(termId, { id: termId, name: term.name, path: term.path }); } }); } } /** * 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.toParent(); } else if (window.targetCheck(e, '.toggle-children')) { let termItem = e.target.closest('li'); this.toChild( parseInt(termItem.dataset.id), termItem.querySelector('.term-name').textContent ); } else if (window.targetCheck(e, '.path-level')) { let pathLevel = window.targetCheck(e, '.path-level'); if (pathLevel.textContent !== this.currentParentName) { 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 and initialize */ openModal() { if (!this.activeField || !this.currentConfig) { console.error('No active field set for modal'); return; } this.resetModalState(); this.updateModalForTaxonomy(); this.observer.observe(this.elements.sentinel); // Set up search if enabled if (this.currentConfig.canSearch) { this.elements.searchInput.focus(); this.searchHandler = window.debounce(() => this.handleSearch(), 300); this.elements.searchInput.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(); // Fetch terms this.fetchTerms(this.activeField); } /** * Close modal and save selections */ closeModal() { this.observer.unobserve(this.elements.sentinel); window.removeChildren(this.elements.termsList); // Save selections to active field if (this.activeField) { this.saveSelectionsToField(this.activeField); } // Cleanup if (this.currentConfig?.canSearch && this.searchHandler) { this.elements.searchInput.removeEventListener('input', this.searchHandler); } if (this.creator) { delete this.creator; } this.activeField = null; this.currentConfig = null; } /** * Reset modal state */ resetModalState() { this.page = 1; this.hasMore = true; this.isLoading = false; this.searchQuery = ''; this.navigationPath = []; this.currentParent = 0; this.currentParentName = ''; this.disabled = false; window.removeChildren(this.elements.termsList); window.removeChildren(this.elements.selectedTerms); this.elements.searchInput.value = ''; } /** * Update modal content for current taxonomy */ updateModalForTaxonomy() { if (!this.currentConfig) return; this.elements.modalTitle.textContent = `Select ${this.currentConfig.plural}`; const searchWrapper = this.modal.querySelector('.search-wrapper'); if (searchWrapper) { searchWrapper.style.display = this.currentConfig.canSearch ? 'block' : 'none'; } if (this.elements.createNewSection) { this.elements.createNewSection.style.display = this.currentConfig.canCreate ? 'block' : 'none'; this.elements.createNewSection.hidden = !this.currentConfig.canCreate; } const openMessage = `Opened ${this.currentConfig.singular} selection. Choose from checkboxes or search to filter results.`; this.a11y?.announce(openMessage); } /** * Update modal selections display */ updateModalSelections() { window.removeChildren(this.elements.selectedTerms); this.selectedTerms.forEach((termData, id) => { this.addTermToModalDisplay(id, termData.name, termData.path); }); this.checkSelectionLimits(); } /** * 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.elements.termsList.querySelector(`input[value="${id}"]`); if (checkbox) { checkbox.checked = true; } } /** * Remove selected term from modal */ removeSelectedTermFromModal(id) { this.selectedTerms.delete(id); // Remove from modal display const selectedItem = this.elements.selectedTerms.querySelector(`[data-id="${id}"]`); if (selectedItem) { selectedItem.remove(); } // Uncheck the corresponding checkbox const checkbox = this.elements.termsList.querySelector(`input[value="${id}"]`); if (checkbox) { checkbox.checked = false; } this.checkSelectionLimits(); } /** * 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.elements.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 */ setCheckboxes(disabled) { this.elements.termsList.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; // 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)); this.updateFieldDisplay(fieldId, selectedIds); } } /** * Handle search input */ handleSearch() { let query = this.elements.searchInput.value.trim(); if (query !== this.searchQuery) { this.searchQuery = query; this.page = 1; this.hasMore = true; window.removeChildren(this.elements.termsList); if (query.length >= 2 || query.length === 0) { this.fetchTerms(this.activeField, false, true); } else { this.hideLoading(); this.showEmptyState('No Results. \nEnter at least 2 characters to search.'); } } } /** * Navigate to parent term */ toParent() { this.navigationPath.pop(); let parent = this.navigationPath[this.navigationPath.length - 1]; this.currentParent = parent ? parent.id : 0; this.currentParentName = parent ? parent.name : ''; window.removeChildren(this.elements.termsList); this.page = 1; this.hasMore = true; this.fetchTerms(this.activeField); } /** * Navigate to child term */ toChild(id, name) { this.navigationPath.push({ id: id, name: name }); this.currentParent = id; this.currentParentName = name; window.removeChildren(this.elements.termsList); this.page = 1; this.hasMore = true; this.fetchTerms(this.activeField); } /** * Navigate to specific path level */ navigateToPath(pathLevel) { window.removeChildren(this.elements.termsList); let level = parseInt(pathLevel.dataset.level); this.navigationPath = this.navigationPath.slice(0, level + 1); this.currentParent = parseInt(pathLevel.dataset.id); this.currentParentName = pathLevel.textContent; this.page = 1; this.hasMore = true; this.fetchTerms(this.activeField); } /** * Fetch terms for the active field */ async fetchTerms(fieldId, forceRefresh = false, isSearch = false) { if (this.isLoading || !fieldId) return; const field = this.fields.get(fieldId); if (!field) return; if (!this.stores.has(field.taxonomy)) { this.stores.set(field.taxonomy, new window.jvbStore({ name: field.taxonomy, endpoint: 'terms', filters: { taxonomy: field.taxonomy, page: 1, search: '', parent: 0 } })); } let store = this.stores.get(field.taxonomy); store.fetch(); return; try { this.showLoading(); const requestUrl = `${jvbSettings.api}terms?${this.buildRequest(field.taxonomy)}`; const response = await this.cache.fetchWithCache(requestUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': jvbSettings.nonce } }, { content: field.taxonomy, forceRefresh: forceRefresh }); // Handle specific term fetching if (this.fetchSpecificTerms) { this.fetchSpecificTerms = false; return response.terms || []; } if (response && response.terms && response.terms.length !== 0) { this.hasMore = response.pagination?.has_more || false; this.renderTerms(response.terms, this.page > 1, isSearch); if (this.hasMore) { this.nextPage(); } } else { if (this.page === 1) { this.showEmptyState(); } this.hasMore = false; } return response.terms || []; } catch (error) { this.handleError(error); return []; } finally { this.hideLoading(); } } /** * Build request parameters */ buildRequest(taxonomy = null) { let params = new URLSearchParams({ taxonomy: taxonomy || this.currentConfig?.taxonomy || '', parent: this.currentParent, search: this.searchQuery, page: this.page }); if (this.fetchSpecificTerms) { params.append('termIDs', this.fetchSpecificTerms); } return params.toString(); } /** * Render terms list */ renderTerms(terms, append = false, showPath = false) { if (!append) { window.removeChildren(this.elements.termsList); } if (terms.length === 0) { if (!append) { this.showEmptyState(); } this.a11y?.announce(0, append); return; } this.updateBreadcrumbs(); for (const [index, term] of Object.entries(terms)) { if (!term) continue; // Store term in global cache const taxonomy = this.currentConfig.taxonomy; if (!this.terms.has(taxonomy)) { this.terms.set(taxonomy, new Map()); } this.terms.get(taxonomy).set(term.id, term); this.createTermElement({ id: parseInt(term.id), name: term.name, hasChildren: term.hasChildren, path: term.path || null, show: showPath }); } this.a11y?.announce(terms.length, append); } /** * Create individual term element */ createTermElement(termData) { if (!termData || !termData.name) return; 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('span, .term-name'); 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; 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 ? window.getTemplate('termChildrenToggle') : this.createChildrenToggle(); if (childrenToggle) { childrenToggle.ariaLabel = `View sub-terms of ${termData.name}`; listItem.appendChild(childrenToggle); } } this.elements.termsList.appendChild(listItem); } /** * Create children toggle button */ createChildrenToggle() { const button = document.createElement('button'); button.type = 'button'; button.className = 'toggle-children'; button.innerHTML = '→'; return button; } /** * Update breadcrumb navigation */ updateBreadcrumbs() { window.removeChildren(this.elements.breadcrumbs); this.elements.breadcrumbs.appendChild(this.elements.backButton); this.elements.backButton.hidden = this.currentParent === 0; this.navigationPath.forEach((pathItem, index) => { const breadcrumb = window.getTemplate ? window.getTemplate('termBreadcrumb') : this.createBreadcrumbElement(); breadcrumb.dataset.level = index; breadcrumb.dataset.id = pathItem.id; breadcrumb.title = pathItem.path || pathItem.name; breadcrumb.textContent = pathItem.name; this.elements.breadcrumbs.appendChild(breadcrumb); }); } /** * Create breadcrumb element */ createBreadcrumbElement() { const button = document.createElement('button'); button.type = 'button'; button.className = 'path-level'; return button; } /** * Show loading state */ showLoading() { this.isLoading = true; this.elements.loading.hidden = false; this.modal.classList.add('loading'); let message = this.searchQuery !== '' ? `searching for "${this.searchQuery}" items` : this.currentParentName === '' ? 'loading items' : `loading ${this.currentParentName} items`; if (window.typeLoop) { this.stopTyping = window.typeLoop(this.elements.loadingText, message); } else { this.elements.loadingText.textContent = message; } } /** * Hide loading state */ hideLoading() { this.isLoading = false; this.elements.loading.hidden = true; this.modal.classList.remove('loading'); if (this.stopTyping) { this.stopTyping(); } } /** * Show empty state message */ showEmptyState(message = '') { const emptyElement = window.getTemplate('noResults'); if (message && emptyElement.querySelector('span')) { emptyElement.querySelector('span').textContent = message; } this.elements.termsList.appendChild(emptyElement); } /** * Move to next page */ nextPage() { if (this.hasMore) { this.page++; } } /** * Handle API errors */ handleError(error) { console.error('Taxonomy selector error:', error); if (this.error && this.error.log) { return this.error.log(error, { component: 'TaxonomySelector', action: 'fetchTerms' }, () => this.fetchTerms(this.activeField)); } else { this.showEmptyState('Error loading terms. Please try again.'); } } /** * Initialize prefetching system */ initPrefetching() { if (document.readyState === 'complete') { this.startPrefetching(); } else { window.addEventListener('load', () => { this.scheduleIdlePrefetch(); }); } } /** * Schedule prefetching during browser idle time */ scheduleIdlePrefetch() { if ('requestIdleCallback' in window) { requestIdleCallback((deadline) => { this.startPrefetching(deadline); }, { timeout: 5000 }); } else { setTimeout(() => this.startPrefetching(), 1000); } } /** * Start the prefetching process */ startPrefetching() { const visibleTaxonomies = new Set(); document.querySelectorAll('button.taxonomy-toggle').forEach(button => { visibleTaxonomies.add(button.dataset.taxonomy); }); visibleTaxonomies.forEach(taxonomy => { this.prefetchTaxonomyTerms(taxonomy); }); } /** * Prefetch terms for a specific taxonomy */ async prefetchTaxonomyTerms(taxonomy) { try { const response = await this.cache.fetchWithCache( `${jvbSettings.api}terms?taxonomy=${taxonomy}&page=1`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': jvbSettings.nonce } }, { content: taxonomy } ); if (response.terms) { if (!this.terms.has(taxonomy)) { this.terms.set(taxonomy, new Map()); } response.terms.forEach(term => { this.terms.get(taxonomy).set(term.id, term); }); } } catch (error) { console.warn(`Failed to prefetch terms for ${taxonomy}:`, error); } } /** * Get field ID from any element within the field */ getFieldId(element) { // Try multiple approaches to find the field ID if (element.dataset.fieldId) { return element.dataset.fieldId; } const fieldContainer = element.closest('[data-field-id]'); if (fieldContainer) { return fieldContainer.dataset.fieldId || fieldContainer.dataset.field || fieldContainer.dataset.name; } return null; } /** * Utility: delay for prefetching */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Clean up */ destroy() { // Remove event listeners document.removeEventListener('click', this.handleClick); document.removeEventListener('change', this.handleChange); // Clear intervals and cleanup this.observer?.disconnect(); // Clear all maps this.fields.clear(); this.terms.clear(); this.selectedTerms.clear(); } } /** * Initialize singleton */ document.addEventListener('DOMContentLoaded', function() { if (!window.jvbSelector) { window.jvbSelector = new TaxonomySelector(); } });