From 552d48a1424417da160c4952650ea6f4a3d7bafa Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 04 Jan 2026 20:18:23 +0000
Subject: [PATCH] =Taxonomy Selector and Creator refactor

---
 assets/js/concise/TaxonomySelector.js | 1433 +++++++++++++++++++++++++++--------------------------------
 1 files changed, 661 insertions(+), 772 deletions(-)

diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index b13e9ae..b411d11 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -1,6 +1,6 @@
 /**
- * Centralized Taxonomy Selector with DataStore Integration
- * Handles all taxonomy selection fields using DataStore for state management
+ * TaxonomySelector - Streamlined version
+ * Manages taxonomy selection fields with DataStore integration
  */
 class TaxonomySelector {
 	constructor() {
@@ -8,105 +8,136 @@
 		this.error = window.jvbError;
 		this.index = -1;
 
-		// DataStore instances per taxonomy
-		this.stores = new Map();
-		this.storeSubscriptions = new Map();
+		this.isInitializing = true;
+		this.taxonomiesToFetch = new Set();
+		this.subscribers = new Set();
 
-		// Central field management
+		// Register DataStore
+		const store = window.jvbStore.register('taxonomies', {
+			storeName: 'terms',
+			keyPath: 'id',
+			showLoading: false,
+			indexes: [
+				{name: 'taxonomy', keyPath: 'taxonomy'},
+				{name: 'parent', keyPath: 'parent'},
+				{name: 'slug', keyPath: 'slug', unique: true},
+				{name: 'count', keyPath: 'count'},
+			],
+			endpoint: 'terms',
+			TTL: 2 * 60 * 1000,
+			filters: {
+				taxonomy: '',
+				page: 1,
+				search: '',
+				parent: 0
+			},
+			required: 'taxonomy',
+			delayFetch: true,
+		});
+		this.store = store.terms;
+
+		// Field management
 		this.fields = new Map();
-		this.selectedTerms = new Map();    // Current modal selection
+		this.selectedTerms = new Map(); // Current modal selection
 
-		// Current modal context
+		// Modal context
 		this.activeField = null;
 		this.currentConfig = null;
-		this.currentSingular = null;
-		this.currentPlural = null;
-		this.activeStore = null;
-
-		// Modal state
 		this.disabled = false;
 
-		// Search debouncing
-		this.searchHandler = null;
+		// Search contexts
+		this.searchContexts = new Map();
 
 		this.init();
 	}
 
-	/**
-	 * Initialize the selector
-	 */
 	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();
 	}
 
-	/**
-	 * Get or create a DataStore for a taxonomy
-	 */
-	getOrCreateStore(taxonomy) {
-		if (!this.stores.has(taxonomy)) {
-			const store = new window.jvbStore({
-				name: `tax_${taxonomy}`,
-				endpoint: 'terms',
-				TTL: 3600000, // 1 hour cache
-				filters: {
-					taxonomy: taxonomy,
-					page: 1,
-					search: '',
-					parent: 0
+	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);
 				}
 			});
-
-			// Subscribe to store events
-			const unsubscribe = store.subscribe((event, data) => {
-				this.handleStoreEvent(taxonomy, event, data);
-			});
-
-			this.stores.set(taxonomy, store);
-			this.storeSubscriptions.set(taxonomy, unsubscribe);
 		}
 
-		return this.stores.get(taxonomy);
+		// Render based on context
+		this.renderSearchResults(data);
 	}
 
-	/**
-	 * Handle DataStore events
-	 */
-	handleStoreEvent(taxonomy, event, data) {
-		// Only process events for the active taxonomy in modal
-		if (this.activeStore && this.activeStore.config.name === `tax_${taxonomy}`) {
-			switch (event) {
-				case 'items-loaded':
-				case 'data-fetched':
-				case 'data-cached':
-				case 'stale-cache-used':
-					this.handleTermsLoaded(data);
-					break;
-				case 'fetch-error':
-					this.handleFetchError(data.error);
-					break;
-				case 'filters-changed':
-					// Could trigger UI updates for active filters
-					break;
-			}
-		}
+	renderSearchResults(data) {
+		const context = this.getActiveSearchContext();
 
-		// Handle field-specific updates outside modal
-		if (event === 'items-updated' || event === 'items-loaded') {
-			this.updateFieldsForTaxonomy(taxonomy, data.items);
+		if (context === 'modal') {
+			this.renderModalResults(data);
+		} else if (context === 'autocomplete') {
+			this.renderAutocompleteResults(data);
 		}
 	}
 
-	/**
-	 * Handle loaded terms from DataStore
-	 */
-	handleTermsLoaded(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 = data.data?.items || [];
-		const pagination = data.data?.pagination || {};
-		const isSearch = data.filters?.search && data.filters.search.length > 0;
-		const append = data.filters?.page > 1;
+		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) {
@@ -115,91 +146,96 @@
 			this.observer.unobserve(this.ui.sentinel);
 		} else {
 			this.renderTerms(terms, append, isSearch);
-			this.currentTerms = terms;
-			// Handle pagination
-			if (pagination.has_more) {
+
+			if (response.has_more) {
 				this.observer.observe(this.ui.sentinel);
 			} else {
 				this.observer.unobserve(this.ui.sentinel);
 			}
 		}
 
-		// Announce to screen readers
 		this.a11y?.announce(terms.length, append);
 	}
 
-	/**
-	 * Handle fetch errors
-	 */
-	handleFetchError(error) {
-		console.error('Taxonomy fetch error:', error);
-		this.hideLoading();
+	renderAutocompleteResults(data) {
+		const field = this.fields.get(this.activeField);
+		if (!field?.autocompleteDropdown) return;
 
-		if (this.error?.log) {
-			this.error.log(error, {
-				component: 'TaxonomySelector',
-				action: 'fetchTerms'
-			}, () => this.fetchCurrentTerms());
-		} else {
-			this.showEmptyState('Error loading terms. Please try again.');
+		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();
 		}
 	}
 
-	/**
-	 * Update fields when taxonomy items are updated
-	 */
-	updateFieldsForTaxonomy(taxonomy, items) {
-		this.fields.forEach(field => {
-			if (field.taxonomy === taxonomy && field.selectedTerms.size > 0) {
-				// Update display with fresh term data
-				field.selectedTerms.forEach(termId => {
-					const term = items.find(item => item.id === termId);
-					if (term) {
-						const selectedItem = field.selectedContainer.querySelector(`[data-id="${termId}"]`);
-						if (selectedItem) {
-							selectedItem.dataset.path = term.path;
-							selectedItem.querySelector('span').textContent = term.path;
-						}
-					}
-				});
-			}
+	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);
 		});
 	}
 
-	/**
-	 * Scan page for existing taxonomy fields and register them
-	 */
-	scanExistingFields() {
-		const selectors = document.querySelectorAll('.field.taxonomy, .field.post');
-		selectors.forEach(selector => {
+	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.error.log(error, {
-					component: 'TaxonomySelector',
-					action: 'scanExistingFields',
-					container: selector.dataset.name
-				});
+				this.handleError(error, 'scanExistingFields', selector.dataset.name);
 			}
 		});
 	}
 
-	/**
-	 * Register a taxonomy field
-	 */
-	registerField(field, options = {}) {
-		let input = field.querySelector('input[type=hidden]');
-		if (!input) {
-			return;
-		}
+	registerField(field) {
+		const input = field.querySelector('input[type=hidden]');
+		if (!input) return false;
 
-		if (!('fieldId' in field.dataset)) {
-			field.dataset.fieldId = this.createFieldId(field);
-		}
-		let fieldId = field.dataset.fieldId;
-		let button = field.querySelector('button.taxonomy-toggle');
+		const fieldId = this.createFieldId(field);
+		field.dataset.fieldId = fieldId;
 
-		let config = {
+		const button = field.querySelector('button.taxonomy-toggle');
+		const config = {
 			id: fieldId,
 			input: input,
 			container: field,
@@ -207,29 +243,32 @@
 			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'),
-			...options
 		};
 
-		// Parse initial selected values
+		// Parse initial values
 		const value = input.value.trim();
-		if (value !== '') {
-			const selectedIds = value.split(',')
+		if (value) {
+			value.split(',')
 				.map(id => parseInt(id.trim()))
-				.filter(id => !isNaN(id));
-			selectedIds.forEach(id => config.selectedTerms.add(id));
+				.filter(id => !isNaN(id))
+				.forEach(id => config.selectedTerms.add(id));
 		}
 
 		this.fields.set(fieldId, config);
 
-		// Ensure store exists for this taxonomy
-		this.getOrCreateStore(config.taxonomy);
+		// Queue for batch fetch
+		if (this.isInitializing) {
+			this.taxonomiesToFetch.add(config.taxonomy);
+		}
 
-		// Initialize display for any pre-selected values
+		// Initialize display
 		if (config.selectedTerms.size > 0) {
 			this.initFieldDisplay(fieldId);
 		}
@@ -237,71 +276,29 @@
 		return fieldId;
 	}
 
-	/**
-	 * Create unique field ID
-	 */
 	createFieldId(field) {
 		this.index++;
 		return 'selector-' + this.index;
 	}
 
-	/**
-	 * Initialize display for a field with existing values
-	 */
 	async initFieldDisplay(fieldId) {
 		const field = this.fields.get(fieldId);
 		if (!field || field.selectedTerms.size === 0) return;
 
-		const store = this.getOrCreateStore(field.taxonomy);
-		const selectedIds = Array.from(field.selectedTerms);
-
-		// Check store for cached terms first
-		const cachedTerms = [];
-		const needsFetch = [];
-
-		selectedIds.forEach(termId => {
-			const term = store.getItem(termId);
+		Array.from(field.selectedTerms).forEach(termId => {
+			const term = this.store.get(termId);
 			if (term) {
-				cachedTerms.push(term);
-			} else {
-				needsFetch.push(termId);
+				this.addTermDisplay(termId, term.name, term.path, 'field', fieldId);
 			}
 		});
-
-		// Display cached terms immediately
-		cachedTerms.forEach(term => {
-			this.addTermToDisplay(fieldId, term.id, term.name, term.path);
-		});
-
-		// Fetch missing terms if needed
-		if (needsFetch.length > 0) {
-			try {
-				const response = await store.fetch('terms', {
-					filters: {
-						taxonomy: field.taxonomy,
-						termIDs: needsFetch.join(',')
-					}
-				});
-
-				if (response.terms) {
-					response.terms.forEach(term => {
-						store.setItem(term.id, term);
-						this.addTermToDisplay(fieldId, term.id, term.name, term.path);
-					});
-				}
-			} catch (error) {
-				console.error('Failed to fetch missing terms:', error);
-			}
-		}
 	}
 
-	/**
-	 * Initialize modal elements
-	 */
-	initModal() {
-		this.modalID = 'dialog#jvb-selector';
-		this.modal = document.querySelector(this.modalID);
+	/***********************************************************************
+	 * MODAL INITIALIZATION
+	 ***********************************************************************/
 
+	initModal() {
+		this.modal = document.querySelector('dialog#jvb-selector');
 		if (!this.modal) {
 			console.warn('Taxonomy selector modal not found');
 			return;
@@ -309,37 +306,24 @@
 
 		this.initModalElements();
 
-		// Initialize modal instance
 		this.modalInstance = new window.jvbModal(this.modal, {
-			handleForm: false,
-			save: null,
-			open: null
+			handleForm: false
 		});
-		this.modalInstance.subscribe((event, data) => {
-			switch (event) {
-				case 'modal-open':
-					console.log(data);
-					this.openModal(data);
-					break;
-				case 'modal-close':
-					this.closeModal(data);
-					break;
-			}
+
+		this.modalInstance.subscribe((event) => {
+			if (event === 'modal-open') this.openModal();
+			if (event === 'modal-close') this.closeModal();
 		});
 	}
 
-	/**
-	 * Initialize modal element references
-	 */
 	initModalElements() {
-		this.selectors = {
+		const selectors = {
 			search: {
 				input: '[type=search]',
-				clear: '.clear-search',
 				container: '.search-wrapper'
 			},
-			termsList: 	'.items-container',
-			termsWrap:	'.items-wrap',
+			termsList: '.items-container',
+			termsWrap: '.items-wrap',
 			breadcrumbs: {
 				nav: 'nav.term-navigation',
 				back: '.back-to-parent',
@@ -352,28 +336,23 @@
 			sentinel: '.scroll-sentinel',
 			modal: {
 				title: '#modal-title',
-				content: '.modal-content'
 			},
 			create: {
 				details: '.create-new-term',
-				parent: '#select_parent',
 				summary: '.create-new-term summary',
-				name: '#term_name',
-				button: '.submit-term',
 				label: {
 					name: '[for=term_name]',
 					parent: '[for=select_parent]'
 				}
-			},
-			favouriteTerms: '.favourite-terms'
-		}
+			}
+		};
 
-		this.ui = window.uiFromSelectors(this.selectors);
+		this.ui = window.uiFromSelectors(selectors);
 
-		// Initialize intersection observer for infinite scroll
+		// Initialize infinite scroll observer
 		this.observer = new IntersectionObserver((entries) => {
 			entries.forEach(entry => {
-				if (entry.isIntersecting && this.activeStore) {
+				if (entry.isIntersecting) {
 					this.loadMoreTerms();
 				}
 			});
@@ -383,27 +362,29 @@
 		});
 	}
 
-	/**
-	 * Set up global event delegation
-	 */
+	/***********************************************************************
+	 * GLOBAL EVENT LISTENERS
+	 ***********************************************************************/
+
 	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);
 	}
 
-	/**
-	 * Handle global click events
-	 */
 	handleClick(e) {
-		// Handle taxonomy toggle buttons
-		const toggleButton = window.targetCheck(e, '.taxonomy-toggle');
-		if (toggleButton) {
+		// Toggle button
+		if (window.targetCheck(e, '.taxonomy-toggle')) {
 			e.preventDefault();
-			this.handleToggleClick(toggleButton);
+			const fieldId = this.getFieldId(e.target);
+			const field = this.fields.get(fieldId);
+			if (field) this.setActiveField(fieldId, true);
 			return;
 		}
 
-		// Handle remove selected term buttons
+		// Remove selected term
 		const removeButton = window.targetCheck(e, 'button.remove-item');
 		if (removeButton && e.target.closest('.jvb-selector')) {
 			const fieldId = this.getFieldId(removeButton);
@@ -412,25 +393,20 @@
 			return;
 		}
 
-		// Handle modal close button
+		// Modal close
 		if (e.target.matches('.modal-close')) {
-			if (this.modalInstance) {
-				this.modalInstance.handleClose();
-			}
+			this.modalInstance?.handleClose();
 			return;
 		}
 
-		// Handle clicks within the modal
-		if (this.modal && this.modal.contains(e.target)) {
+		// Modal clicks
+		if (this.modal?.contains(e.target)) {
 			this.handleModalClick(e);
 		}
 	}
 
-	/**
-	 * Handle global change events
-	 */
 	handleChange(e) {
-		// Handle hidden input changes for taxonomy fields
+		// Hidden input changes
 		const taxonomyField = window.targetCheck(e, '.taxonomy.field, .post.field');
 		if (taxonomyField && e.target.type === 'hidden') {
 			const fieldId = this.getFieldId(e.target);
@@ -438,245 +414,205 @@
 			return;
 		}
 
-		// Handle modal changes
-		if (this.modal && this.modal.contains(e.target)) {
+		// Modal checkboxes
+		if (this.modal?.contains(e.target)) {
 			this.handleModalChange(e);
 		}
 	}
 
-	/**
-	 * Handle toggle button click
-	 */
-	handleToggleClick(toggle) {
-		try {
-			const fieldId = this.getFieldId(toggle);
+	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);
+			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) {
-				console.error('Field not found for toggle:', 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.setActiveField(fieldId);
-			this.modalInstance.handleOpen();
+			this.searchContexts.set(fieldId, 'autocomplete');
+			this.activeField = fieldId;
 
-		} catch (error) {
-			console.error('Error handling toggle click:', error);
-			this.error?.handleError(error, {
-				component: 'TaxonomySelector',
-				action: 'handleToggleClick'
-			});
+			if (field.autocompleteDropdown) {
+				field.autocompleteDropdown.hidden = false;
+			}
 		}
+
+		// Debounced search
+		window.debouncer.schedule(
+			`taxonomy-search-${context}-${fieldId || 'modal'}`,
+			async () => {
+				await this.store.setFilters({
+					taxonomy: field.taxonomy,
+					search: query,
+					page: 1,
+					parent: query ? 0 : (this.store.filters.parent || 0)
+				});
+
+				if (context === 'modal') {
+					window.removeChildren(this.ui.termsList);
+				}
+			},
+			300
+		);
 	}
 
-	/**
-	 * Set the active field for modal operations
-	 */
-	setActiveField(fieldId) {
+	/***********************************************************************
+	 * MODAL OPERATIONS
+	 ***********************************************************************/
+
+	setActiveField(fieldId, openModal = false) {
 		this.activeField = fieldId;
 		this.currentConfig = this.fields.get(fieldId);
 
-		console.log('Current Taxonomy:',this.currentConfig.taxonomy);
-		console.log('Labels: ',jvbSettings.labels[this.currentConfig.taxonomy]);
+		if (openModal) {
+			this.modalInstance.handleOpen();
+		}
 
-		this.currentSingular = jvbSettings.labels[this.currentConfig.taxonomy].single;
-		this.currentPlural = jvbSettings.labels[this.currentConfig.taxonomy].plural;
+		this.store.setFilter('taxonomy', this.currentConfig.taxonomy);
 
-		// Get or create store for this taxonomy
-		this.activeStore = this.getOrCreateStore(this.currentConfig.taxonomy);
-
-		// Clear modal selection state
+		// Reset modal selection state
 		this.selectedTerms.clear();
 
-		// Copy field's current selections to modal state
-		if (this.currentConfig.selectedTerms) {
-			this.currentConfig.selectedTerms.forEach(termId => {
-				const term = this.activeStore.getItem(termId);
-				if (term) {
-					this.selectedTerms.set(termId, {
-						id: termId,
-						name: term.name,
-						path: term.path
-					});
-				} else {
-					// If not in store, create minimal entry
-					this.selectedTerms.set(termId, {
-						id: termId,
-						name: `Term ${termId}`,
-						path: `Term ${termId}`
-					});
-				}
-			});
-		}
+		// 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
+				});
+			}
+		});
 	}
 
-	/**
-	 * Handle clicks within modal
-	 */
 	handleModalClick(e) {
 		if (window.targetCheck(e, '.remove-item')) {
-			let selectedItem = window.targetCheck(e, '.selected-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')) {
-			let termItem = e.target.closest('li');
+			const termItem = e.target.closest('li');
 			this.navigateToChild(
 				parseInt(termItem.dataset.id),
 				termItem.querySelector('.term-name').textContent
 			);
 		} else if (window.targetCheck(e, '.path-level')) {
-			let pathLevel = window.targetCheck(e, '.path-level');
-			this.navigateToPath(pathLevel);
+			const pathLevel = window.targetCheck(e, '.path-level');
+			this.navigateToPath(parseInt(pathLevel.dataset.id) || 0);
 		}
 	}
 
-	/**
-	 * Handle changes within modal (checkboxes)
-	 */
 	handleModalChange(e) {
-		if (window.targetCheck(e, this.modalID) && e.target.type === 'checkbox') {
-			e.preventDefault();
-			e.stopPropagation();
+		if (e.target.type !== 'checkbox') return;
 
-			const termId = parseInt(e.target.closest('li').dataset.id);
-			const label = e.target.closest('li').querySelector('label');
+		e.preventDefault();
+		e.stopPropagation();
 
-			if (e.target.checked) {
-				this.addSelectedTermToModal(termId, label.title, label.dataset.path);
-			} else {
-				this.removeSelectedTermFromModal(termId);
-			}
+		const termId = parseInt(e.target.closest('li').dataset.id);
+		const label = e.target.closest('li').querySelector('label');
+
+		if (e.target.checked) {
+			this.addSelectedTermToModal(termId, label.title, label.dataset.path);
+		} else {
+			this.removeSelectedTermFromModal(termId);
 		}
 	}
 
-	/**
-	 * Open modal for filtering (without a field)
-	 * @param {string} taxonomy - The taxonomy to filter by
-	 * @param {Function} callback - Callback when terms are selected
-	 * @param {Array} preselected - Array of term IDs already selected
-	 */
-	openForFilter(taxonomy, callback, preselected = []) {
-		// Create a temporary virtual field config
-		const virtualFieldId = `filter-${taxonomy}-${Date.now()}`;
-
-		this.fields.set(virtualFieldId, {
-			id: virtualFieldId,
-			input: null, // No input for filter mode
-			container: null,
-			taxonomy: taxonomy,
-			name: `filter_${taxonomy}`,
-			maxSelection: 0, // No limit for filters
-			canSearch: true,
-			canCreate: false, // Disable creation for filters
-			isRequired: false,
-			selectedTerms: new Set(preselected),
-			toggle: null,
-			selectedContainer: null,
-			isFilterMode: true, // Flag for filter mode
-			filterCallback: callback // Store the callback
-		});
-
-		this.setActiveField(virtualFieldId);
-		this.modalInstance.handleOpen();
-	}
-
-	/**
-	 * Open modal and initialize
-	 */
 	openModal() {
-		if (!this.activeField || !this.currentConfig) {
-			console.error('No active field set for modal');
+		if (!this.currentConfig) {
+			console.error('No active field set');
 			return;
 		}
 
-		this.resetModalState();
-		this.updateModalForTaxonomy();
-
-		// Reset store filters to default state
-		this.activeStore.clearFilters();
-
-		// Set up search if enabled
-		if (this.currentConfig.canSearch) {
-			this.ui.search.input.focus();
-			this.searchHandler = window.debounce(() => this.handleSearch(), 300);
-			this.ui.search.input.addEventListener('input', this.searchHandler);
-		}
-
-		// Initialize creator if available
-		if (this.currentConfig.canCreate && 'jvbTaxCreator' in window) {
-			this.creator = new window.jvbTaxCreator(this);
-		}
-
-		// Display current selections
+		this.updateModalUI();
 		this.updateModalSelections();
 
-		// Start observing for infinite scroll
-		this.observer.observe(this.ui.sentinel);
-
-		// Fetch initial terms
-		this.fetchCurrentTerms();
+		window.removeChildren(this.ui.termsList);
+		this.showLoading();
 	}
 
-	/**
-	 * Close modal and save selections
-	 */
 	closeModal() {
 		this.observer.unobserve(this.ui.sentinel);
 		window.removeChildren(this.ui.termsList);
 
-		if (this.currentConfig?.isFilterMode) {
-			// Call the filter callback with selected terms
-			if (this.currentConfig.filterCallback) {
-				const selectedIds = Array.from(this.selectedTerms.keys());
-				this.currentConfig.filterCallback(selectedIds, this.currentConfig.taxonomy);
-			}
+		this.notify('selected-terms', {
+			terms: this.selectedTerms,
+			taxonomy: this.currentConfig.taxonomy
+		});
 
-			// Clean up the virtual field
-			this.fields.delete(this.activeField);
-		} else if (this.activeField) {
+		if (this.activeField) {
 			this.saveSelectionsToField(this.activeField);
 		}
 
-		// Cleanup
-		if (this.currentConfig?.canSearch && this.searchHandler) {
-			this.ui.search.input.removeEventListener('input', this.searchHandler);
-		}
-
-		if (this.creator) {
-			delete this.creator;
-		}
-
-		this.activeStore = null;
 		this.activeField = null;
 		this.currentConfig = null;
 	}
 
-	/**
-	 * Reset modal state
-	 */
-	resetModalState() {
-		this.disabled = false;
+	updateModalUI() {
+		const singular = this.getLabel(this.currentConfig.taxonomy, 'single');
+		const plural = this.getLabel(this.currentConfig.taxonomy, 'plural');
 
-		window.removeChildren(this.ui.termsList);
-		window.removeChildren(this.ui.selectedTerms);
-		this.ui.search.input.value = '';
-
-		// Clear navigation breadcrumbs
-		window.removeChildren(this.ui.breadcrumbs.nav);
-		this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back);
-		this.ui.breadcrumbs.back.hidden = true;
-	}
-
-	/**
-	 * Update modal content for current taxonomy
-	 */
-	updateModalForTaxonomy() {
-		if (!this.currentConfig) return;
-
-		this.ui.modal.title.textContent = `Select ${this.currentPlural}`;
+		this.ui.modal.title.textContent = `Select ${plural}`;
 
 		if (this.ui.search.container) {
 			this.ui.search.container.style.display = this.currentConfig.canSearch ? 'block' : 'none';
@@ -687,185 +623,126 @@
 			this.ui.create.details.hidden = !this.currentConfig.canCreate;
 
 			if (this.ui.create.summary) {
-				this.ui.create.summary.textContent = `Add new ${this.currentSingular}`;
+				this.ui.create.summary.textContent = `Add new ${singular}`;
 			}
 
 			if (this.ui.create.label.name) {
-				this.ui.create.label.name.textContent = `Name this ${this.currentSingular}`;
+				this.ui.create.label.name.textContent = `Name this ${singular}`;
 			}
 			if (this.ui.create.label.parent) {
 				this.ui.create.label.parent.textContent = `Nest it under`;
 			}
-
-			if (this.ui.create.parent) {
-
-			}
 		}
 
-		const openMessage = `Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;
-		this.a11y?.announce(openMessage);
+		this.a11y?.announce(`Opened ${singular} selection. Choose from checkboxes or search to filter results.`);
 	}
 
-	/**
-	 * Update modal selections display
-	 */
 	updateModalSelections() {
 		window.removeChildren(this.ui.selectedTerms);
 
 		this.selectedTerms.forEach((termData, id) => {
-			this.addTermToModalDisplay(id, termData.name, termData.path);
+			this.addTermDisplay(id, termData.name, termData.path, 'modal');
 		});
 
 		this.checkSelectionLimits();
 	}
 
-	/**
-	 * Add selected term to modal
-	 */
 	addSelectedTermToModal(id, name, path) {
-		this.selectedTerms.set(id, {
-			id: id,
-			name: name,
-			path: path
-		});
+		this.selectedTerms.set(id, { id, name, path });
 
-		this.addTermToModalDisplay(id, name, path);
+		this.addTermDisplay(id, name, path, 'modal');
 		this.checkSelectionLimits();
 
-		// Check the corresponding checkbox
 		const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
-		if (checkbox) {
-			checkbox.checked = true;
-		}
+		if (checkbox) checkbox.checked = true;
 	}
 
-	/**
-	 * Remove selected term from modal
-	 */
 	removeSelectedTermFromModal(id) {
 		this.selectedTerms.delete(parseInt(id));
 
-		// Remove from modal display
 		const selectedItem = this.ui.selectedTerms.querySelector(`[data-id="${id}"]`);
-		if (selectedItem) {
-			selectedItem.remove();
-		}
+		if (selectedItem) selectedItem.remove();
 
-		// Uncheck the corresponding checkbox
 		const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
-		if (checkbox) {
-			checkbox.checked = false;
-		}
+		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.ui.selectedTerms.appendChild(item);
-	}
-
-	/**
-	 * Check selection limits and disable/enable checkboxes
-	 */
 	checkSelectionLimits() {
 		if (!this.currentConfig || this.currentConfig.maxSelection === 0) {
 			return;
 		}
 
 		this.disabled = this.selectedTerms.size >= this.currentConfig.maxSelection;
-		this.setCheckboxes(this.disabled);
-	}
 
-	/**
-	 * Set checkbox disabled state
-	 */
-	setCheckboxes(disabled) {
 		this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
 			if (!checkbox.checked) {
-				checkbox.disabled = disabled;
+				checkbox.disabled = this.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);
+			this.addTermDisplay(id, termData.name, termData.path, 'field', fieldId);
 		});
 
-		// Update hidden input
-		const selectedIds = Array.from(field.selectedTerms);
-		field.input.value = selectedIds.join(',');
+		field.input.value = Array.from(field.selectedTerms).join(',');
 		field.input.dispatchEvent(new Event('change', { bubbles: true }));
 	}
 
-	/**
-	 * Remove selected term from field
-	 */
+	/***********************************************************************
+	 * TERM DISPLAY
+	 ***********************************************************************/
+
+	addTermDisplay(termId, termName, termPath, context = 'field', fieldId = null) {
+		const config = context === 'field'
+			? this.fields.get(fieldId)
+			: this.currentConfig;
+
+		const container = context === 'field'
+			? config.selectedContainer
+			: this.ui.selectedTerms;
+
+		if (container.querySelector(`[data-id="${termId}"]`)) 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}`;
+
+		container.appendChild(item);
+
+		if (context === 'modal') {
+			const checkbox = this.ui.termsList.querySelector(`input[value="${termId}"]`);
+			if (checkbox) checkbox.checked = true;
+		}
+	}
+
 	removeSelectedTerm(fieldId, termId) {
 		const field = this.fields.get(fieldId);
 		if (!field) return;
 
-		const id = parseInt(termId);
-		field.selectedTerms.delete(id);
+		field.selectedTerms.delete(parseInt(termId));
 
-		// Remove from display
-		const selectedItem = field.selectedContainer.querySelector(`[data-id="${id}"]`);
-		if (selectedItem) {
-			selectedItem.remove();
-		}
+		const selectedItem = field.selectedContainer.querySelector(`[data-id="${termId}"]`);
+		if (selectedItem) selectedItem.remove();
 
-		// Update hidden input
-		const selectedIds = Array.from(field.selectedTerms);
-		field.input.value = selectedIds.join(',');
+		field.input.value = Array.from(field.selectedTerms).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;
@@ -874,232 +751,52 @@
 		field.selectedTerms.clear();
 		window.removeChildren(field.selectedContainer);
 
-		if (value !== '') {
-			const selectedIds = value.split(',')
+		if (value) {
+			value.split(',')
 				.map(id => parseInt(id.trim()))
-				.filter(id => !isNaN(id));
+				.filter(id => !isNaN(id))
+				.forEach(id => field.selectedTerms.add(id));
 
-			selectedIds.forEach(id => field.selectedTerms.add(id));
 			this.initFieldDisplay(fieldId);
 		}
 	}
 
-	/**
-	 * Handle search input
-	 */
-	handleSearch() {
-		const query = this.ui.searchInput.value.trim();
+	/***********************************************************************
+	 * NAVIGATION
+	 ***********************************************************************/
 
-		if (query.length >= 2 || query.length === 0) {
-			// Reset pagination when searching
-			this.activeStore.setFilter('page', 1);
-			this.activeStore.setFilter('search', query);
-
-			window.removeChildren(this.ui.termsList);
-
-			if (query.length >= 2) {
-				this.showLoading();
-				this.fetchCurrentTerms();
-			} else if (query.length === 0) {
-				// Clear search and reload
-				this.showLoading();
-				this.fetchCurrentTerms();
-			}
-		} else {
-			this.hideLoading();
-			this.showEmptyState('Enter at least 2 characters to search.');
-		}
-	}
-
-	/**
-	 * Navigate to parent term
-	 */
 	navigateToParent() {
-		const currentParent = this.activeStore.filters.parent || 0;
-
-		// Find parent of current parent (could enhance this with breadcrumb tracking)
-		this.activeStore.setFilter('parent', 0);
-		this.activeStore.setFilter('page', 1);
-
+		this.store.setFilters({ parent: 0, page: 1 });
 		window.removeChildren(this.ui.termsList);
-		this.showLoading();
-		this.fetchCurrentTerms();
-
-		// Update breadcrumbs
 		this.ui.breadcrumbs.back.hidden = true;
 	}
 
-	/**
-	 * Navigate to child term
-	 */
 	navigateToChild(termId, termName) {
-		this.activeStore.setFilter('parent', termId);
-		this.activeStore.setFilter('page', 1);
-
+		this.store.setFilters({ parent: termId, page: 1 });
 		window.removeChildren(this.ui.termsList);
-		this.showLoading();
-		this.fetchCurrentTerms();
-
-		// Update breadcrumbs
 		this.updateBreadcrumbs(termId, termName);
 		this.ui.breadcrumbs.back.hidden = false;
 	}
 
-	/**
-	 * Navigate to specific path level
-	 */
-	navigateToPath(pathLevel) {
-		const parentId = parseInt(pathLevel.dataset.id) || 0;
-
-		this.activeStore.setFilter('parent', parentId);
-		this.activeStore.setFilter('page', 1);
-
+	navigateToPath(parentId) {
+		this.store.setFilters({ parent: parentId, page: 1 });
 		window.removeChildren(this.ui.termsList);
-		this.showLoading();
-		this.fetchCurrentTerms();
-
-		// Update breadcrumbs to this level
-		// You'd need to track the full path to properly implement this
 		this.ui.breadcrumbs.back.hidden = parentId === 0;
 	}
 
-	/**
-	 * Fetch terms using current store filters
-	 */
-	fetchCurrentTerms() {
-		if (!this.activeStore) return;
-
-		this.showLoading();
-		this.activeStore.fetch();
-	}
-
-	/**
-	 * Load more terms (pagination)
-	 */
 	loadMoreTerms() {
-		if (!this.activeStore) return;
-
-		const currentPage = this.activeStore.filters.page || 1;
-		this.activeStore.setFilter('page', currentPage + 1);
-		// fetch() will be called automatically by setFilter
+		const currentPage = this.store.filters.page || 1;
+		this.store.setFilter('page', currentPage + 1);
 	}
 
-	/**
-	 * Render terms list
-	 */
-	renderTerms(terms, append = false, showPath = false) {
-		if (!append) {
-			window.removeChildren(this.ui.termsList);
-		}
-
-		if (terms.length === 0) {
-			if (!append) {
-				this.showEmptyState();
-			}
-			return;
-		}
-
-		// Update breadcrumbs if needed
-		const currentParent = this.activeStore.filters.parent || 0;
-		this.ui.breadcrumbs.back.hidden = currentParent === 0;
-
-		terms.forEach(term => {
-			// Check if we have a cached DOM element
-			const cachedElement = this.activeStore.getDOMElement(term.id, 'list-item');
-
-			if (cachedElement) {
-				// Update checkbox state if needed
-				const checkbox = cachedElement.querySelector('input[type="checkbox"]');
-				if (checkbox) {
-					checkbox.checked = this.selectedTerms.has(term.id);
-					checkbox.disabled = !checkbox.checked && this.disabled;
-				}
-				this.ui.termsList.appendChild(cachedElement);
-			} else {
-				// Create new element and cache it
-				const element = this.createTermElement({
-					id: parseInt(term.id),
-					name: term.name,
-					hasChildren: term.hasChildren,
-					path: term.path || null,
-					show: showPath
-				});
-
-				if (element) {
-					this.activeStore.storeDOMElement(term.id, 'list-item', element);
-					this.ui.termsList.appendChild(element);
-				}
-			}
-		});
-	}
-
-	/**
-	 * Create individual term element
-	 */
-	createTermElement(termData) {
-		if (!termData || !termData.name) return null;
-
-		const listItem = window.getTemplate('termListItem').cloneNode(true);
-		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);
-			}
-		}
-
-		return 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(termId, termName) {
-		// This is a simplified version - you'd want to maintain a proper breadcrumb trail
-		const breadcrumb = window.getTemplate('termBreadcrumb').cloneNode(true);
+		const breadcrumb = window.getTemplate('termBreadcrumb');
 		breadcrumb.dataset.id = termId;
 		breadcrumb.textContent = termName;
 		breadcrumb.title = termName;
 
-		// Remove any existing breadcrumbs after this level
 		const existingCrumb = this.ui.breadcrumbs.nav.querySelector(`[data-id="${termId}"]`);
 		if (existingCrumb) {
-			// Remove all breadcrumbs after this one
 			while (existingCrumb.nextElementSibling) {
 				existingCrumb.nextElementSibling.remove();
 			}
@@ -1108,21 +805,172 @@
 		}
 	}
 
-	/**
-	 * Show loading state
-	 */
+	/***********************************************************************
+	 * 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();
+			return;
+		}
+
+		const currentParent = this.store.filters.parent || 0;
+		this.ui.breadcrumbs.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
+			});
+
+			if (element) fragment.appendChild(element);
+		});
+
+		this.ui.termsList.appendChild(fragment);
+	}
+
+	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 listItem;
+	}
+
+	/***********************************************************************
+	 * AUTOCOMPLETE
+	 ***********************************************************************/
+
+	showAutocompleteResults(field, terms, query) {
+		if (!field?.autocompleteDropdown) return;
+
+		const dropdown = field.autocompleteDropdown;
+		window.removeChildren(dropdown);
+
+		if (terms.length === 0) {
+			this.showEmptyState('No items found.', dropdown);
+		} else {
+			const fragment = document.createDocumentFragment();
+
+			terms.forEach(term => {
+				const item = this.createAutocompleteItem(field, term);
+				if (item) fragment.appendChild(item);
+			});
+
+			dropdown.appendChild(fragment);
+		}
+
+		// 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));
+			}
+		}
+
+		dropdown.hidden = false;
+	}
+
+	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;
+	}
+
+	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) {
+		const field = this.fields.get(fieldId);
+		if (!field?.autocompleteDropdown) return;
+
+		window.removeChildren(field.autocompleteDropdown);
+		this.showEmptyState('Hmmm... something went wrong', field.autocompleteDropdown);
+	}
+
+	/***********************************************************************
+	 * UI STATES
+	 ***********************************************************************/
+
 	showLoading() {
 		this.ui.loading.loading.hidden = false;
 		this.modal.classList.add('loading');
 
-		const searchQuery = this.activeStore?.filters?.search || '';
-		const currentParent = this.activeStore?.filters?.parent || 0;
+		const searchQuery = this.store.filters.search || '';
+		const currentParent = this.store.filters.parent || 0;
 
-		let message = searchQuery !== '' ?
-			`searching for "${searchQuery}" items` :
-			currentParent === 0 ?
-				'loading items' :
-				`loading child items`;
+		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);
@@ -1131,9 +979,6 @@
 		}
 	}
 
-	/**
-	 * Hide loading state
-	 */
 	hideLoading() {
 		this.ui.loading.loading.hidden = true;
 		this.modal.classList.remove('loading');
@@ -1143,65 +988,109 @@
 		}
 	}
 
-	/**
-	 * Show empty state message
-	 */
-	showEmptyState(message = 'No items found.') {
-		const emptyElement = window.getTemplate('noResults').cloneNode(true);
+	showEmptyState(message = 'No items found.', container = null) {
+		if (!container) container = this.ui.termsList;
 
-		if (message && emptyElement.querySelector('span')) {
-			emptyElement.querySelector('span').textContent = message;
+		const emptyElement = window.getTemplate('noResults');
+		const messageSpan = emptyElement.querySelector('span');
+
+		if (message && messageSpan) {
+			messageSpan.textContent = message;
 		}
 
-		this.ui.termsList.appendChild(emptyElement);
+		container.appendChild(emptyElement);
 	}
 
-	/**
-	 * Get field ID from any element within the field
-	 */
+	/***********************************************************************
+	 * UTILITIES
+	 ***********************************************************************/
+
 	getFieldId(element) {
-		if (element.dataset.fieldId) {
-			return element.dataset.fieldId;
-		}
+		if (element.dataset.fieldId) return element.dataset.fieldId;
 
 		const fieldContainer = element.closest('[data-field-id]');
-		if (fieldContainer) {
-			return fieldContainer.dataset.fieldId;
-		}
-
-		return null;
+		return fieldContainer?.dataset.fieldId || null;
 	}
 
-	/**
-	 * Clean up
-	 */
+	getLabel(taxonomy, type = 'single') {
+		return jvbSettings.labels[taxonomy]?.[type] || taxonomy;
+	}
+
+	async batchFetchTaxonomies() {
+		if (this.taxonomiesToFetch.size === 0) return;
+
+		const taxonomies = Array.from(this.taxonomiesToFetch);
+		this.taxonomiesToFetch.clear();
+
+		this.store.setFilters({
+			taxonomy: taxonomies.join(','),
+			page: 1,
+			search: '',
+			parent: 0
+		});
+	}
+
+	async preloadTaxonomy(taxonomy) {
+		await this.store.setFilters({
+			taxonomy: taxonomy,
+			page: 1,
+			search: '',
+			parent: 0
+		});
+	}
+
+	handleError(error, context, detail = null) {
+		console.error(`Taxonomy ${context} error:`, error, detail);
+
+		if (this.error?.log) {
+			this.error.log(error, {
+				component: 'TaxonomySelector',
+				action: context,
+				detail: detail
+			});
+		}
+
+		if (this.modal?.open) {
+			this.showEmptyState('Error loading. Please try again.');
+		}
+	}
+
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data = {}) {
+		this.subscribers.forEach(callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
+			}
+		});
+	}
+
 	destroy() {
-		// Remove event listeners
 		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);
 
-		// Clear intervals and cleanup
 		this.observer?.disconnect();
-
-		// Unsubscribe from all stores
-		this.storeSubscriptions.forEach(unsubscribe => unsubscribe());
-
-		// Destroy all stores
-		this.stores.forEach(store => store.destroy());
-
-		// Clear all maps
+		this.store.destroy();
+		this.subscribers.clear();
 		this.fields.clear();
-		this.stores.clear();
-		this.storeSubscriptions.clear();
 		this.selectedTerms.clear();
+		this.searchContexts.clear();
 	}
 }
 
-/**
- * Initialize singleton
- */
-document.addEventListener('DOMContentLoaded', function() {
-	if (!window.jvbSelector) {
-		window.jvbSelector = new TaxonomySelector();
-	}
+// Initialize on auth ready
+document.addEventListener('DOMContentLoaded', () => {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbSelector = new TaxonomySelector();
+		}
+	});
 });

--
Gitblit v1.10.0