From 9f86429a1252b45c95b7c62fbaa1b82de3723997 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 05 Jan 2026 18:16:07 +0000
Subject: [PATCH] =Complete TaxonomySelector.js and TaxonomyCreator.js refactor

---
 assets/js/concise/TaxonomySelector.js | 2140 +++++++++++++++++++++-------------------------------------
 1 files changed, 783 insertions(+), 1,357 deletions(-)

diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index 8ae44bb..3cdaf4d 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -1,25 +1,41 @@
-/**
- * Centralized Taxonomy Selector with DataStore Integration
- * Handles all taxonomy selection fields using DataStore for state management
- */
 class TaxonomySelector {
 	constructor() {
+		this.container = document.querySelector('dialog#jvb-selector');
+		if (!this.container) return;
+
 		this.a11y = window.jvbA11y;
 		this.error = window.jvbError;
-		this.index = -1;
-
-		this.hasAutocomplete = false;
-		this.isInitializing = true;
-		this.taxonomiesToFetch = new Set();
-
-		this.triggers = new Set(['.taxonomy-toggle']);
 
 		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.init();
+	}
+
+	init() {
+		this.initStore();
+		this.initElements();
+		this.initModal();
+		this.scanExistingFields();
+		this.initListeners();
+
+		if (this.needsCreator() && window.jvbTaxCreator) {
+			this.creator = new window.jvbTaxCreator(this);
+		}
+		this.isInitializing = false
+		this.batchFetchTaxonomies().then(()=> {});
+	}
+
+	initStore() {
 		const store = window.jvbStore.register(
 			'taxonomies',
 			{
-				storeName: `terms`,
+				storeName: 'terms',
 				keyPath: 'id',
 				showLoading: false,
 				indexes: [
@@ -29,7 +45,7 @@
 					{name: 'count', keyPath: 'count'},
 				],
 				endpoint: 'terms',
-				TTL: 2 * 60 * 1000, //2 hours
+				TTL: 2 * 60 * 1000,
 				filters: {
 					taxonomy: '',
 					page: 1,
@@ -38,1492 +54,911 @@
 				},
 				required: 'taxonomy',
 				delayFetch: true,
-			});
+			}
+		);
 		this.store = store.terms;
 
-		// Central field management
-		this.fields = new Map();
-		this.selectedTerms = new Map();    // Current modal selection
-
-		// Current modal context
-		this.activeField = null;
-		this.currentConfig = null;
-		this.currentSingular = null;
-		this.currentPlural = null;
-
-		// Modal state
-		this.disabled = false;
-
-		// Search debouncing
-		this.searchHandler = null;
-		this.autocompleteHandler = null;
-		this.isAutocompleteActive = false;
-
-		this.init();
-	}
-
-	/**
-	 * Initialize the selector
-	 */
-	init() {
-		this.initModal();
-		this.scanExistingFields();
-		this.initGlobalListeners();
-
-		if (this.hasAutocomplete && window.jvbTaxCreator) {
-			this.creator = new window.jvbTaxCreator(this);
-		}
 		this.store.subscribe(this.handleStoreEvent.bind(this));
-		// Complete initialization
-		this.isInitializing = false;
-		this.batchFetchTaxonomies();
 	}
 
-	/**
-	 * Handle DataStore events
-	 */
-	handleStoreEvent(event, data) {
-		switch (event) {
-			case 'data-loaded':
-				const taxonomy = this.store.filters.taxonomy;
-				// Handle batch taxonomy loading (comma-separated)
-				if (taxonomy?.includes(',')) {
-					this.handleBatchDataLoaded(taxonomy, data);
-				}
-				// Update button states for this taxonomy (or taxonomies)
-				if (taxonomy) {
-					// Handle comma-separated taxonomies from batch fetch
-					const taxonomies = taxonomy.includes(',')
-						? taxonomy.split(',').map(t => t.trim())
-						: [taxonomy];
-
-					taxonomies.forEach(tax => {
-						this.updateFieldsForTaxonomy(tax);
-					});
-				}
-
-				// Only render if modal is open OR autocomplete active
-				if (this.modal?.open) {
-					this.handleTermsLoaded(data);
-				}
-
-				if (this.isAutocompleteActive && this.activeField) {
-					const field = this.fields.get(this.activeField);
-					const terms = data.data?.items || [];
-					const query = data.filters?.search || '';
-					this.showAutocompleteResults(field, terms, query);
-					this.isAutocompleteActive = false;
-				}
-				break;
-
-			case 'filters-changed':
-				if (this.modal?.open) {
-					this.showLoading();
-				}
-				break;
-
-			case 'fetch-error':
-				if (this.isAutocompleteActive && this.activeField) {
-					this.showAutocompleteError(this.activeField);
-					this.isAutocompleteActive = false;
-				}
-				this.handleFetchError(data.error);
-				break;
-		}
-	}
-
-	/**
-	 * Handle loaded terms from DataStore
-	 */
-	handleTermsLoaded(data) {
-		this.hideLoading();
-		const terms = this.store.getFiltered();  // Use getFiltered() instead of getFilteredItems()
-		const response = this.store.lastResponse?.page || {};
-		const isSearch = data.filters?.search && data.filters.search.length > 0;
-		const append = response.page > 1;
-
-		this.notify('terms-loaded', { terms, filters: data.filters });
-
-		if (terms.length === 0) {
-			if (!append) {
-				this.showEmptyState(isSearch ? 'No results found.' : 'No items available.');
-			}
-			this.observer.unobserve(this.ui.sentinel);
-		} else {
-			this.renderTerms(terms, append, isSearch);
-
-			// Handle pagination
-			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();
-
-		if (this.error?.log) {
-			this.error.log(error, {
-				component: 'TaxonomySelector',
-				action: 'fetchTerms'
-			}, () => this.fetchCurrentTerms());
-		} else {
-			this.showEmptyState('Error loading terms. Please try again.');
-		}
-	}
-
-
-	/**
-	 * Check if taxonomy has terms and update button states
-	 */
-	updateFieldButtonState(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Check store for items of this specific taxonomy
-		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.getSingular(field.taxonomy)} available`
-				: `Select ${this.getPlural(field.taxonomy)}`;
-		}
-	}
-	/**
-	 * Update fields when taxonomy items are updated
-	 */
-	updateFieldsForTaxonomy(taxonomy) {
-		this.getFieldsForTaxonomy(taxonomy).forEach(field => {
-			this.updateFieldButtonState(field.id);
-		});
-	}
-
-	/**
-	 * Get fields for a specific taxonomy
-	 */
-	getFieldsForTaxonomy(taxonomy) {
-		return Array.from(this.fields.values())
-			.filter(field => field.taxonomy === taxonomy);
-	}
-
-
-
-	/**
-	 * Scan page for existing taxonomy fields and register them
-	 */
-	scanExistingFields(container = null) {
-		if (!container) {
-			container = document.body;
-		}
-		const selectors = container.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 false;
-		}
-		if (!('fieldId' in field.dataset)) {
-			field.dataset.fieldId = this.createFieldId(field);
-		}
-		let fieldId = field.dataset.fieldId;
-
-		let button = (Object.hasOwn(options, 'button')) ? options.button : field.querySelector('button.taxonomy-toggle');
-
-		if (Object.hasOwn(options, 'buttonSelector')) {
-			this.triggers.add(options.buttonSelector);
-		}
-
-		let config = {
-			id: fieldId,
-			input: input,
-			container: field,
-			taxonomy: button.dataset.taxonomy,
-			name: field.dataset.field,
-			maxSelection: parseInt(button.dataset.max) || 0,
-			canSearch: 'search' in button.dataset,
-			hasAutocomplete: 'autocomplete' in button.dataset,
-			autocompleteDropdown: field.querySelector('.autocomplete-dropdown')??false,
-			canCreate: 'creatable' in button.dataset,
-			isRequired: 'required' in button.dataset,
-			selectedTerms: new Set(),
-			toggle: button,
-			selectedContainer: (Object.hasOwn(options, 'selected')) ? options.selected : field.querySelector('.selected-items'),
-			...options
-		};
-
-		if (!this.hasAutocomplete && config.hasAutocomplete) {
-			this.hasAutocomplete = true;
-			this.initAutocomplete();
-		}
-
-		// Parse initial selected values
-		const value = input.value.trim();
-		if (value !== '') {
-			const selectedIds = value.split(',')
-				.map(id => parseInt(id.trim()))
-				.filter(id => !isNaN(id));
-			selectedIds.forEach(id => config.selectedTerms.add(id));
-		}
-
-		if (Object.hasOwn(options, 'selectedItems')) {
-			options.selectedItems.forEach(id => {
-				config.selectedTerms.add(id);
-			});
-		}
-
-		this.fields.set(fieldId, config);
-
-		// Ensure store exists for this taxonomy
-		if (this.isInitializing) {
-			this.taxonomiesToFetch.add(config.taxonomy);
-		} else {
-			// this.store.setFilter('taxonomy', config.taxonomy);
-		}
-
-		// Initialize display for any pre-selected values
-		if (config.selectedTerms.size > 0) {
-			this.initFieldDisplay(fieldId);
-		}
-
-		return fieldId;
-	}
-
-	/**
-	 * Register a filter button (simplified registration for feed blocks)
-	 */
-	registerFilterButton(button, options = {}) {
-		const fieldId = this.createFieldId(button);
-		button.dataset.fieldId = fieldId;
-
-		if (options.buttonSelector) {
-			this.triggers.add(options.buttonSelector);
-		}
-
-		const config = {
-			id: fieldId,
-			input: null,
-			container: options.container || button.closest('.filters') || button.parentElement,
-			taxonomy: button.dataset.taxonomy,
-			name: `filter_${button.dataset.taxonomy}`,
-			maxSelection: parseInt(button.dataset.max) || 0,
-			canSearch: 'search' in button.dataset,
-			hasAutocomplete: false,
-			canCreate: false,
-			isRequired: false,
-			selectedTerms: new Set(options.selectedItems || []),
-			toggle: button,
-			selectedContainer: options.selected || null,
-			isFilterMode: true,
-			...options
-		};
-
-		this.fields.set(fieldId, config);
-
-		if (this.isInitializing) {
-			this.taxonomiesToFetch.add(config.taxonomy);
-		} else {
-			this.store.setFilter('taxonomy', config.taxonomy);
-		}
-
-		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 selectedIds = Array.from(field.selectedTerms);
-
-		selectedIds.forEach(termId => {
-			const term = this.store.get(termId);  // Changed from getItem
-			if (term) {
-				this.addTermToDisplay(fieldId, term.id, term.name, term.path);
-			}
-		});
-	}
-
-	/**
-	 * Initialize modal elements
-	 */
-	initModal() {
-		this.modalID = 'dialog#jvb-selector';
-		this.modal = document.querySelector(this.modalID);
-
-		if (!this.modal) {
-			console.warn('Taxonomy selector modal not found');
-			return;
-		}
-
-		this.initModalElements();
-
-		// Initialize modal instance
-		this.modalInstance = new window.jvbModal(this.modal, {
-			handleForm: false,
-			save: null,
-			open: null
-		});
-		this.modalInstance.subscribe((event, data) => {
-			switch (event) {
-				case 'modal-open':
-					this.openModal(data);
-					break;
-				case 'modal-close':
-					this.closeModal(data);
-					break;
-			}
-		});
-	}
-
-	/**
-	 * Initialize modal element references
-	 */
-	initModalElements() {
+	/******************************************************************
+	 ELEMENTS
+	 ******************************************************************/
+	initElements() {
 		this.selectors = {
 			search: {
 				input: '[type=search]',
 				clear: '.clear-search',
-				container: '.search-wrapper'
+				container: '.search-wrapper',
+				results: '.search-results'
 			},
-			termsList: 	'.items-container',
-			termsWrap:	'.items-wrap',
-			breadcrumbs: {
+			terms: {
+				list: '.items-container',
+				wrap: '.items-wrap',
+				sentinel: '.scroll-sentinel',
+			},
+			nav: {
 				nav: 'nav.term-navigation',
 				back: '.back-to-parent',
+				child: '.toggle-children',
+				pathLevel: '.path-level',
 			},
 			loading: {
 				loading: '.loading',
-				text: '.loading span'
+				text: '.loading span',
 			},
-			selectedTerms: '.selected-items',
-			sentinel: '.scroll-sentinel',
+			selected: '.selected-items',
 			modal: {
 				title: '#modal-title',
-				content: '.modal-content'
+				content: '.modal-content',
+				count: '.selection-count'
 			},
-			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'
+			favourites: '.favourite-terms',
+			field: {
+				toggle: 'button.taxonomy-toggle',
+				value: 'input[type="hidden"]',
+				selected: '.selected-items',
+				dropdown: '.search-results',
+				search: '[data-autocomplete]',
+			}
 		}
 
 		this.ui = window.uiFromSelectors(this.selectors);
+	}
 
-		// Initialize intersection observer for infinite scroll
+	initListeners() {
 		this.observer = new IntersectionObserver((entries) => {
 			entries.forEach(entry => {
 				if (entry.isIntersecting) {
-					this.loadMoreTerms();
+					this.nextPage();
 				}
 			});
 		}, {
-			root: this.ui.termsWrap,
+			root: this.ui.terms.sentinel,
 			threshold: 0.5
 		});
+
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+		this.inputHandler = this.handleInput.bind(this);
+		this.focusHandler = this.handleFocus.bind(this);
+		this.blurHandler = this.handleBlur.bind(this);
+
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+		document.addEventListener('input', this.inputHandler);
+		document.addEventListener('focus', this.focusHandler, true);
+		document.addEventListener('blur', this.blurHandler, true);
 	}
 
-	/**
-	 * Set up global event delegation
-	 */
-	initGlobalListeners() {
-		document.addEventListener('click', this.handleClick.bind(this));
-		document.addEventListener('change', this.handleChange.bind(this));
-		if (this.hasAutocomplete) {
-			this.initAutocomplete();
-		}
-	}
+	handleClick(e) {
+		const fieldId = this.getFieldId(e.target);
+		const field = this.fields.get(fieldId);
+		if (!fieldId || !field) return;
 
-	initAutocomplete()
-	{
-		this.autocompleteHandler = (e) => {
-			window.debouncer.schedule(
-				'taxonomy-autocomplete',
-				() => this.handleAutocomplete(e),
-				300
-			);
-		};
-		document.addEventListener('input', this.autocompleteHandler);
-		document.addEventListener('blur', this.cleanupAutocomplete.bind(this));
-		// Preload taxonomy data on focus
-		document.addEventListener('focus', (e) => {
-			if (!('autocomplete' in e.target.dataset)) {
-				return;
+		const autoComplete = window.targetCheck(e, '[data-autocomplete-select]');
+		if (autoComplete) {
+			let termId = parseInt(autoComplete.dataset.id);
+			this.addSelected(termId, fieldId);
+			if (field.ui.dropdown) {
+				field.ui.dropdown.hidden = true;
 			}
 
-			const fieldId = this.getFieldId(e.target);
-			const field = this.fields.get(fieldId);
+			if (field.ui.search) {
+				field.ui.search.value = '';
+			}
+		}
 
-			if (!field) return;
-
-			// Preload this taxonomy's data
-			this.preloadTaxonomy(field.taxonomy);
-		}, true); // Use capture phase
-	}
-
-	/**
-	 * Handle global click events
-	 */
-	handleClick(e) {
-		// Handle taxonomy toggle buttons
-		const toggleButton = window.targetCheck(e, Array.from(this.triggers));
-
+		const toggleButton = window.targetCheck(e, field.ui.toggle);
 		if (toggleButton) {
 			e.preventDefault();
-			this.handleToggleClick(toggleButton);
+			this.openModal(fieldId);
 			return;
 		}
 
-		// Handle remove selected term buttons
 		const removeButton = window.targetCheck(e, 'button.remove-item');
-		if (removeButton && e.target.closest('.jvb-selector')) {
+		if (removeButton) {
 			const fieldId = this.getFieldId(removeButton);
-			const termId = removeButton.closest('.selected-item').dataset.id;
-			this.removeSelectedTerm(fieldId, termId);
+			const termId = removeButton.closest('.selected-item').dataset.id??false;
+			if (fieldId && termId) {
+				this.removeSelected(termId, fieldId);
+			}
 			return;
 		}
 
-		// Handle modal close button
 		if (e.target.matches('.modal-close')) {
-			if (this.modalInstance) {
-				this.modalInstance.handleClose();
-			}
+			this.modal?.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, true);
-
-		} catch (error) {
-			console.error('Error handling toggle click:', error);
-			if (this.error?.log) {
-				this.error.log(error, {
-					component: 'TaxonomySelector',
-					action: 'handleToggleClick'
-				});
-			}
-		}
-	}
-
-	/**
-	 * Set the active field for modal operations
-	 */
-	setActiveField(fieldId, openModal = false) {
-		this.activeField = fieldId;
-		this.currentConfig = this.fields.get(fieldId);
-
-		this.currentSingular = this.getSingular(this.currentConfig.taxonomy);
-		this.currentPlural = this.getPlural(this.currentConfig.taxonomy);
-
-		if (openModal) {
-			this.modalInstance.handleOpen();
-		}
-
-		// Set taxonomy filter - store handles the rest
-		this.store.setFilter('taxonomy', this.currentConfig.taxonomy);
-
-		// Clear modal selection state
-		this.selectedTerms.clear();
-
-		// Copy field's current selections to modal state
-		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');
-			if (selectedItem) {
-				this.removeSelectedTermFromModal(selectedItem.dataset.id);
-			}
-		} else if (window.targetCheck(e, '.back-to-parent')) {
+		const backToParent = window.targetCheck(e, this.selectors.nav.back);
+		if (backToParent) {
 			this.navigateToParent();
-		} else if (window.targetCheck(e, '.toggle-children')) {
-			let termItem = e.target.closest('li');
-			this.navigateToChild(
-				parseInt(termItem.dataset.id),
-				termItem.querySelector('.term-name').textContent
-			);
-		} else if (window.targetCheck(e, '.path-level')) {
-			let pathLevel = window.targetCheck(e, '.path-level');
-			this.navigateToPath(pathLevel);
-		}
-	}
-
-	/**
-	 * Handle changes within modal (checkboxes)
-	 */
-	handleModalChange(e) {
-		if (window.targetCheck(e, this.modalID) && e.target.type === 'checkbox') {
-			e.preventDefault();
-			e.stopPropagation();
-
-			const termId = parseInt(e.target.closest('li').dataset.id);
-			const label = e.target.closest('li').querySelector('label');
-
-			if (e.target.checked) {
-				this.addSelectedTermToModal(termId, label.title, label.dataset.path);
-			} else {
-				this.removeSelectedTermFromModal(termId);
-			}
-		}
-	}
-
-	/**
-	 * Open modal for filtering (without a field)
-	 * @param {string} taxonomy - The taxonomy to filter by
-	 * @param {Function} callback - Callback when terms are selected
-	 * @param {Array} preselected - Array of term IDs already selected
-	 */
-	openForFilter(taxonomy, callback, preselected = []) {
-		// Create a temporary virtual field config
-		const virtualFieldId = `filter-${taxonomy}-${Date.now()}`;
-
-		this.fields.set(virtualFieldId, {
-			id: virtualFieldId,
-			input: null, // No input for filter mode
-			container: null,
-			taxonomy: taxonomy,
-			name: `filter_${taxonomy}`,
-			maxSelection: 0, // No limit for filters
-			canSearch: true,
-			hasAutocomplete: false,
-			autocompleteDropdown: document.querySelector('.autocomplete-dropdown')??false,
-			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, true);
-		this.modalInstance.handleOpen();
-	}
-
-	/**
-	 * Open modal and initialize
-	 */
-	openModal() {
-		if (!this.currentConfig) {
-			console.error('No active field set');
 			return;
 		}
 
-		// Initialize creator if available
-		if (!this.creator && this.currentConfig.canCreate && 'jvbTaxCreator' in window) {
-			this.creator = new window.jvbTaxCreator(this);
+		const toChild = window.targetCheck(e, this.selectors.nav.child);
+		if (toChild) {
+			const termItem = e.target.closest('li');
+			const termId = parseInt(termItem.dataset.id);
+
+			if (termId) {
+				this.navigateTo(termId);
+			}
+			return;
 		}
 
-		// Update modal UI
-		this.updateModalForTaxonomy();
+		const pathLevel = window.targetCheck(e, this.selectors.nav.pathLevel);
+		if (pathLevel) {
+			const termId = parseInt(pathLevel.dataset.id)??0;
+			this.navigateTo(termId);
+		}
 
-		// Load selected terms display
-		this.updateModalSelections();
-		this.updateSelectionCount();
+		const dropdown = window.targetCheck(e, field.selectors.dropdown);
+		if (dropdown) {
+			// reset the timer for hiding the dropdown
+			this.scheduleHideDropdown(fieldId);
+			return;
+		}
 
-		// Clear terms list and show loading
-		window.removeChildren(this.ui.termsList);
-		this.showLoading();
-	}
-
-	/**
-	 * Update selection count display in modal
-	 */
-	updateSelectionCount() {
-		if (!this.currentConfig) return;
-
-		const count = this.selectedTerms.size;
-		const max = this.currentConfig.maxSelection;
-
-		// Update any count display elements
-		const countElement = this.modal?.querySelector('.selection-count');
-		if (countElement) {
-			if (max > 0) {
-				countElement.textContent = `${count} of ${max} selected`;
-			} else {
-				countElement.textContent = `${count} selected`;
+		const clearSearch = window.targetCheck(e, this.selectors.search.clear);
+		if (clearSearch) {
+			const field = this.currentField();
+			if (field && field.ui.search) {
+				field.ui.search.value = '';
+				this.store.setFilters({
+					search: '',
+					page: 1,
+					parent: this.store.filters.parent || 0
+				});
+			}
+			if (this.ui.search.input) {
+				this.ui.search.input.value = '';
 			}
 		}
+
+	}
+	handleChange(e) {
+		if (!this.container.contains(e.target)) {
+			return;
+		}
+		if (e.target.type !== 'checkbox') return;
+		e.preventDefault();
+		e.stopPropagation();
+
+		const termId = parseInt(e.target.dataset.id);
+		let fieldId = this.getFieldId(e.target);
+		if (e.target.checked) {
+			this.addSelected(termId, fieldId);
+		} else {
+			this.removeSelected(termId, fieldId);
+		}
+	}
+	//For search in modal or field autocomplete
+	handleInput(e) {
+		let fieldId = this.getFieldId(e.target)??this.activeField;
+		if (!fieldId) return;
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		if (!this.container.open) {
+			this.activeField = fieldId;
+		}
+
+		const query = e.target.value.trim();
+		window.debouncer.schedule(
+			`${fieldId}-search`,
+			async () => {
+				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
+		);
 	}
 
+	handleFocus(e) {
+		const fieldId = this.getFieldId(e.target);
+		const field = this.fields.get(fieldId);
+		if (!fieldId || !field) return;
+		if (!field.hasAutocomplete && !field.hasSearch) return;
 
+		window.debouncer.cancel(`${fieldId}-search-results`);
 
-	/**
-	 * Get singular label for taxonomy
-	 */
-	getSingular(taxonomy) {
-		return jvbSettings.labels[taxonomy]?.single || taxonomy;
+		if (!this.container.open){
+			this.activeField = fieldId;
+			this.preloadTaxonomy(field.taxonomy);
+		}
 	}
 
-	/**
-	 * Get plural label for taxonomy
-	 */
-	getPlural(taxonomy) {
-		return jvbSettings.labels[taxonomy]?.plural || taxonomy;
+	//Hide autocomplete dropdown on blur
+	handleBlur(e) {
+		const fieldId = this.getFieldId(e.target);
+		const field = this.fields.get(fieldId);
+		if (!fieldId || ! field) return;
+		if (!field.hasAutocomplete) return;
+
+		this.scheduleHideDropdown(fieldId);
 	}
 
-	/**
-	 * Close modal and save selections
-	 */
-	closeModal() {
-		this.observer.unobserve(this.ui.sentinel);
-		window.removeChildren(this.ui.termsList);
+	scheduleHideDropdown(fieldId){
+		const field = this.fields.get(fieldId);
+		if (!field) return;
 
-		this.notify('selected-terms', {
-			terms: this.selectedTerms,
-			taxonomy: this.currentConfig.taxonomy
+		window.debouncer.schedule(
+			`${fieldId}-search-results`,
+			() => {
+				this.activeField = null;
+				field.ui.dropdown.hidden = true;
+			},
+			1500
+		);
+	}
+
+	/******************************************************************
+	 MODAL
+	 ******************************************************************/
+	initModal() {
+		this.modalID = 'dialog#jvb-selector';
+		this.container = document.querySelector(this.modalID);
+
+		this.modal = new window.jvbModal(
+			this.container,
+			{
+				handleForm: false,
+				save: null,
+				open: null
+			}
+		);
+		this.modal.subscribe((event, data) => {
+			switch (event) {
+
+			}
 		});
-
-		if (this.currentConfig?.isFilterMode) {
-			if (this.currentConfig.filterCallback) {
-				const selectedIds = Array.from(this.selectedTerms.keys());
-				this.currentConfig.filterCallback(selectedIds, this.currentConfig.taxonomy);
-			}
-			// this.fields.delete(this.activeField);
-		} else if (this.activeField) {
-			this.saveSelectionsToField(this.activeField);
-		}
-
-		// Cleanup
-		if (this.currentConfig?.canSearch && this.searchHandler) {
-			this.ui.search.input.removeEventListener('input', this.searchHandler);
-		}
-
-		if (!this.hasAutocomplete && this.creator) {
-			delete this.creator;
-		}
-
-		// Remove: this.activeStore = null;
-		this.activeField = null;
-		this.currentConfig = null;
 	}
 
-	/**
-	 * Reset modal state
-	 */
-	resetModalState() {
-		this.disabled = false;
+	toggleModal(fieldId, open = true) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
 
-		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;
+		if (open) {
+			this.openModal(fieldId);
+		} else {
+			this.closeModal();
+		}
 	}
 
-	/**
-	 * Update modal content for current taxonomy
-	 */
-	updateModalForTaxonomy() {
-		if (!this.currentConfig) return;
+	openModal(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
 
-		this.ui.modal.title.textContent = `Select ${this.currentPlural}`;
-
+		this.activeField = fieldId;
+		this.ui.modal.title.textContent = `Select ${field.plural}`;
 		if (this.ui.search.container) {
-			this.ui.search.container.style.display = this.currentConfig.canSearch ? 'block' : 'none';
+			this.ui.search.container.hidden = !field.canSearch;
 		}
-
 		if (this.ui.create.details) {
-			this.ui.create.details.style.display = this.currentConfig.canCreate ? 'block' : 'none';
-			this.ui.create.details.hidden = !this.currentConfig.canCreate;
+			this.ui.create.details.hidden = !field.canCreate;
 
 			if (this.ui.create.summary) {
-				this.ui.create.summary.textContent = `Add new ${this.currentSingular}`;
+				this.ui.create.summary.textContent = `Add new ${field.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 ${field.singular}`;
 			}
 			if (this.ui.create.label.parent) {
 				this.ui.create.label.parent.textContent = `Nest it under`;
 			}
-
-			if (this.ui.create.parent) {
-
-			}
 		}
+		let message = `Opened ${field.singular} selection. Choose from checkboxes, or search to filter results.`;
 
-		const openMessage = `Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;
-		this.a11y?.announce(openMessage);
-	}
+		window.removeChildren(this.ui.terms.list);
+		this.modal.handleOpen();
+		this.setLoading();
 
-	/**
-	 * Update modal selections display
-	 */
-	updateModalSelections() {
-		window.removeChildren(this.ui.selectedTerms);
-
-		this.selectedTerms.forEach((termData, id) => {
-			this.addTermToModalDisplay(id, termData.name, termData.path);
+		this.store.setFilters({
+			taxonomy: field.taxonomy,
+			page: 1,
+			search: '',
+			parent: 0,
 		});
 
-		this.checkSelectionLimits();
+		this.a11y.announce(message);
 	}
 
-	/**
-	 * Add selected term to modal
-	 */
-	addSelectedTermToModal(id, name, path) {
-		this.selectedTerms.set(id, {
-			id: id,
-			name: name,
-			path: path
+	closeModal() {
+		this.modal.handleClose();
+		const field = this.fields.get(this.activeField);
+		if (!field) return;
+		this.observer.unobserve(this.ui.terms.sentinel);
+		window.removeChildren(this.ui.terms.list);
+
+		this.notify('selected-terms', {
+			terms: this.selectedTerms.get(this.activeField),
+			taxonomy: field.taxonomy
 		});
 
-		this.addTermToModalDisplay(id, name, path);
-		this.checkSelectionLimits();
+		this.activeField = null;
 
-		// Check the corresponding checkbox
-		const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
-		if (checkbox) {
-			checkbox.checked = true;
-		}
+		let message = `Closed ${field.singular} selector.`;
+		this.a11y.announce(message);
 	}
 
-	/**
-	 * 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();
-		}
-
-		// Uncheck the corresponding checkbox
-		const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
-		if (checkbox) {
-			checkbox.checked = false;
-		}
-
-		this.checkSelectionLimits();
+	navigateToParent() {
+		const current = this.store.filters.parent;
+		if (current === 0) return;
+		let term = this.store.get(parseInt(current));
+		if (!term) return;
+		let parent = term.parent;
+		this.navigateTo(parseInt(parent));
+	}
+	navigateTo(termId = 0) {
+		termId = parseInt(termId)??0;
+		this.store.setFilters({parent: termId, page: 1});
+		window.removeChildren(this.ui.terms.list);
+		this.updateBreadcrumbs(termId);
 	}
 
-	/**
-	 * 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;
+	nextPage() {
+		let current = this.store.filters.page;
+		let page = Math.min(current++, this.store.lastResponse.total);
+		this.store.setFilters({page:page});
+	}
+	prevPage() {
+		let current = this.store.filters.page;
+		let page = Math.max(current - 1, 1);
+		this.store.setFilters({page:page});
+	}
+
+	addTermToModal(termId) {
+		const term = this.store.get(termId);
+		if (!term) return;
+
+		const item = window.getTemplate('selectedTerm');
+		item.dataset.id = termId;
+		item.querySelector('span').textContent = term.path;
 		item.querySelector('button').title = `Remove ${name}`;
 
-		this.ui.selectedTerms.appendChild(item);
+		this.ui.selected.append(item);
+	}
+	/******************************************************************
+	 FIELDS
+	 ******************************************************************/
+	scanExistingFields(container = document.body) {
+		container.querySelectorAll('[data-type="selector"]').forEach(
+			selector => {
+				try {
+					this.registerField(selector);
+				} catch (error) {
+					this.error.log(error, {
+						component: 'TaxonomySelector',
+						action: 'scanExistingFields',
+						container: selector.dataset.name
+					});
+				}
+			}
+		);
 	}
 
-	/**
-	 * Check selection limits and disable/enable checkboxes
-	 */
-	checkSelectionLimits() {
-		if (!this.currentConfig || this.currentConfig.maxSelection === 0) {
+	registerField(element, options = {}) {
+		let input = element.querySelector('input[type="hidden"]');
+		if (!input) {
+			console.warn('TaxonomySelector: No hidden input found for field', element);
 			return;
 		}
 
-		this.disabled = this.selectedTerms.size >= this.currentConfig.maxSelection;
-		this.setCheckboxes(this.disabled);
+		if (!('fieldId' in element.dataset)) {
+			element.dataset.fieldId = window.generateID('selector');
+		}
+		const fieldId = element.dataset.fieldId;
+
+
+		let selectors = this.selectors.field;
+		let button = element.querySelector('button.taxonomy-toggle');
+		if (options.size === 0){
+			if (!button) return;
+			options = button.dataset;
+			if (options.size === 0) return;
+		} else if (Object.hasOwn(options, 'toggle')) {
+			button = document.querySelector(options.toggle);
+			selectors.toggle = options.toggle;
+		}
+
+		const config = {
+			id: fieldId,
+			value: input,
+			element: element,
+			taxonomy: options.taxonomy??false,
+			singular: options.single??'',
+			plural: options.plural??'',
+			name: element.dataset.field,
+			canSearch: Object.hasOwn(options, 'search'),
+			limit: options.limit??0,
+			hasAutocomplete: Object.hasOwn(options, 'autocomplete'),
+			canCreate: Object.hasOwn(options, 'creatable'),
+			isRequired: Object.hasOwn(options, 'required'),
+			toggle: button,
+			selectors: selectors,
+			ui: window.uiFromSelectors(selectors, element),
+			checked: false,
+		};
+		if (!config.taxonomy) return;
+		this.fields.set(fieldId, config);
+
+		//Check for stored selected terms in hidden input
+		let selected = new Set();
+		input.value.value.trim()
+			.split(',')
+			.map(id => parseInt(id.trim()))
+			.filter(id => !isNaN(id))
+			.forEach(id => selected.add(id));
+		this.selectedTerms.set(fieldId, selected);
+
+		if (this.isInitializing) {
+			this.batchFetch.add(config.taxonomy);
+		}
+		this.updateFieldUI(fieldId);
+
+		return fieldId;
+	}
+
+	addSelected(termId, fieldId = null) {
+		if (!fieldId) fieldId = this.activeField;
+
+		const field = this.fields.get(fieldId);
+		const term = this.store.get(termId);
+		if (!field || !term) return;
+
+		const selected = this.selectedTerms.get(fieldId);
+		if (field.limit !== 0 && selected.size >= field.limit) return;
+
+		selected.add(parseInt(termId));
+		this.addTermToDisplay(termId, fieldId);
+		this.updateFieldValue(fieldId);
+		this.checkLimits(fieldId);
+	}
+	removeSelected(termId, fieldId = null) {
+		if (!fieldId) fieldId = this.activeField;
+		const field = this.fields.get(fieldId);
+		const term = this.store.get(termId);
+		if (!field || !term) return;
+		this.selectedTerms.get(fieldId).delete(parseInt(termId));
+
+		const selectedItem = field.ui.selected.querySelector(`[data-i"${termId}"]`);
+		if (selectedItem) selectedItem.remove();
+		if (this.container.open) {
+			let item = this.ui.selected.querySelector(`[data-id="${termId}"]`);
+			if (item) item.remove();
+		}
+		this.updateFieldValue(fieldId);
+		this.checkLimits(fieldId);
+	}
+	updateFieldValue(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+		let selected = Array.from(this.selectedTerms.get(fieldId));
+		field.ui.value = selected.join(',');
+	}
+
+	checkLimits(fieldId) {
+		if (!this.container.open) return;
+		const field = this.fields.get(fieldId);
+		if (!field || field.limit === 0) return;
+		const disabled = this.selectedTerms.get(fieldId).size >= field.limit;
+		this.setCheckboxes(disabled);
+	}
+
+	updateFieldUI(fieldId) {
+		const field = this.fields.get(fieldId);
+		let selected = this.selectedTerms.get(fieldId);
+		if (!field || selected.size === 0) return;
+
+		Array.from(selected).forEach(termId => {
+			this.addTermToDisplay(termId, fieldId);
+		});
+	}
+
+	updateFieldsForTaxonomy(taxonomy) {
+		let fields = Array.from(this.fields.values())
+			.filter(field => !field.checked && field.taxonomy === taxonomy);
+		const hasItems = Array.from(this.store.data.values())
+			.some(term=>term.taxonomy === taxonomy);
+
+		fields.forEach(field => {
+			field.ui.toggle.disabled = !hasItems && !field.canCreate;
+			field.ui.toggle.title = !hasItems
+				? `No ${field.singular} available`
+				: `Select ${field.plural}`;
+
+			field.checked = true;
+		});
+	}
+
+	showModalTerms(append = true, showPath = false) {
+		const terms = this.store.getFiltered();
+		if (terms.size === 0) return;
+		if (!append) {
+			window.removeChildren(this.ui.terms.list);
+		}
+
+		const currentParent = this.store.filters.parent??0;
+		this.ui.nav.back.hidden = currentParent === 0;
+
+		const fragment = document.createDocumentFragment();
+		terms.forEach(term => {
+			const element = this.createTermElement({
+				show: showPath,
+				... term
+			});
+			if (element) {
+				fragment.appendChild(element);
+			}
+		});
+
+		this.ui.terms.list.append(fragment);
+	}
+	createTermElement(term) {
+		if (!term || !term.name) return null;
+
+		const item = window.getTemplate('termListItem');
+		item.dataset.id = term.id;
+
+		const isSelected = this.selectedTerms.get(this.activeField).has(term.id);
+		let [
+			checkbox,
+			label,
+			nameSpan
+		] = [
+			item.querySelector('input'),
+			item.querySelector('label'),
+			item.querySelector('span, .term-name')
+		];
+
+		let field = this.currentField();
+		let limitReached = field.limit > 0 && this.selectedTerms.get(this.activeField).size >= field.limit;
+		if (checkbox && label && nameSpan) {
+			[
+				checkbox.id,
+				checkbox.name,
+				checkbox.value,
+				checkbox.disabled,
+				checkbox.checked,
+				label.htmlFor,
+				label.title,
+				label.dataset.path,
+				nameSpan.textContent
+			] = [
+				`${field.element.id}-${term.id}`,
+				`${field.container.id}-${field.taxonomy}-select`,
+				term.id,
+				!isSelected && limitReached,
+				isSelected,
+				`${field.element.id}-${term.id}`,
+				term.path??term.name,
+				term.path,
+				term.show ? term.path : term.name
+			];
+			if (term.hasChildren) {
+				const toggle = window.getTemplate('termChildrenToggle');
+				if (toggle) {
+					toggle.ariaLabel = `View ${field.plural} nested under ${term.name}`;
+					item.append(toggle);
+				}
+			}
+		}
+
+		return item;
+	}
+
+	showAutocompleteTerms() {
+		const field = this.currentField();
+		const terms = this.currentTerms();
+		if (!field || terms.size ===0) return;
+
+		const dropdown = field.ui.dropdown;
+		window.removeChildren(dropdown);
+		if (terms.length === 0) {
+			this.showEmptyState(`No ${field.plural} found.`, dropdown);
+		} else {
+			terms.forEach(term => {
+				const item = this.createAutocompleteTerm(term);
+				if (item) {
+					dropdown.append(item);
+				}
+			})
+		}
+
+		const query = field.ui.search?.value;
+		if (field.canCreate && query.length >= 2 && this.creator) {
+			const createButton = this.createTermButton(query);
+			if (createButton) {
+				dropdown.append(createButton);
+			}
+		}
+
+		dropdown.hidden = false;
+	}
+	createAutocompleteTerm(term) {
+		const item = window.getTemplate('autocompleteItem');
+		if (!item) return;
+
+		item.dataset.id = term.id;
+		item.textContent = term.path || term.name;
+		return item;
+	}
+	/******************************************************************
+	 UI
+	 ******************************************************************/
+	addTermToDisplay(termId, fieldId) {
+		const term = this.store.get(termId);
+		const field = this.fields.get(fieldId);
+		if (!term || !field) return;
+		//if the term already exists in the selected items, bail early
+		if (field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
+
+		const item = window.getTemplate('selectedTerm');
+		if (!item) return;
+
+		item.dataset.id = termId;
+		item.dataset.taxonomy = field.taxonomy;
+		item.querySelector('.item-name').textContent = term.path;
+		item.querySelector('button').title = `Remove ${term.name}`;
+
+		field.ui.selected.append(item);
+
+		if (this.container.open) {
+			this.addTermToModal(termId);
+			const checkbox = this.ui.terms.list.querySelector(`input[value="${termId}"]`);
+			if (checkbox) checkbox.checked = true;
+		}
+	}
+	createTermButton(query) {
+		const button = window.getTemplate('autocompleteButton');
+		if(!button) return;
+
+		let queryEl = button.querySelector('span');
+		queryEl.textContent = `"${query}"`;
+
+		return button;
+	}
+
+	updateBreadcrumbs(termId) {
+		const nav = this.ui.nav.nav;
+		if (!nav) return;
+		const existingCrumb = Array.from(nav.children)
+			.find(crumb => parseInt(crumb.dataset.id) === termId);
+
+		if (existingCrumb) {
+			// Remove all siblings after this crumb
+			let nextSibling = existingCrumb.nextElementSibling;
+			while (nextSibling) {
+				const toRemove = nextSibling;
+				nextSibling = nextSibling.nextElementSibling;
+				toRemove.remove();
+			}
+		} else {
+			// Add new breadcrumb
+			const term = this.store.get(termId);
+			if (!term) return;
+
+			const crumb = window.getTemplate('termBreadcrumb');
+			if (!crumb) return;
+
+			crumb.dataset.id = termId;
+			crumb.textContent = term.name;
+			crumb.title = term.name;
+
+			nav.append(crumb);
+		}
+	}
+
+	updateSelectionCount() {
+		if (!this.container.open) return;
+		const field = this.fields.get(this.activeField);
+		if (!field) return;
+
+		if (this.ui.modal.count) {
+			const total = this.selectedTerms.get(this.activeField).size;
+
+			this.ui.modal.count.textContent = field.limit > 0
+				? `${total} of ${field.limit} ${field.plural} selected`
+				: `${total} ${field.plural} selected`;
+		}
+
+	}
+	/******************************************************************
+	 UTILITY
+	 ******************************************************************/
+	currentField() {
+		return this.fields.get(this.activeField)??false;
+	}
+	currentTerms() {
+		return this.store.getFiltered();
+	}
+	needsCreator() {
+		return Array.from(this.fields.values()).some(field =>
+			field.canCreate || field.hasAutocomplete
+		);
+	}
+
+	getFieldId(element) {
+		if (element.dataset.fieldId) return element.dataset.fieldId;
+
+		const fieldContainer = element.closest('[data-field-id]');
+		return fieldContainer?.dataset.fieldId || null;
 	}
 
 	/**
-	 * Set checkbox disabled state
+	 * Sets all checkbox disabled (or not)
+	 * @param {Boolean} disabled
 	 */
 	setCheckboxes(disabled) {
-		this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
+		this.ui.terms.list.querySelectorAll('input[type=checkbox]').forEach(checkbox => {
 			if (!checkbox.checked) {
 				checkbox.disabled = disabled;
 			}
 		});
 	}
 
-	/**
-	 * Save modal selections to field
-	 */
-	saveSelectionsToField(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
+	/******************************************************************
+	 DATASTORE HELPERS
+	******************************************************************/
+	handleStoreEvent(event, data) {
+		const handlers = {
+			'data-loaded': () => this.handleDataLoaded(),
+			'filters-changed': () => this.handleFiltersChanged(),
+			'fetch-error': () => this.handleFetchError()
+		};
 
-		// Clear current field selections
-		field.selectedTerms.clear();
-		window.removeChildren(field.selectedContainer);
-
-		// Add modal selections to field
-		this.selectedTerms.forEach((termData, id) => {
-			field.selectedTerms.add(id);
-			this.addTermToDisplay(fieldId, id, termData.name, termData.path);
-		});
-
-		// Update hidden input
-		const selectedIds = Array.from(field.selectedTerms);
-		field.input.value = selectedIds.join(',');
-		field.input.dispatchEvent(new Event('change', { bubbles: true }));
+		handlers[event]?.();
 	}
-
-	/**
-	 * 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();
+	handleDataLoaded() {
+		const taxonomy = this.store.filters.taxonomy;
+		if (taxonomy?.includes(',')) {
+			const taxonomies = taxonomy.split(',').map(t => t.trim());
+			taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
 		}
 
-		// Update hidden input
-		const selectedIds = Array.from(field.selectedTerms);
-		field.input.value = selectedIds.join(',');
-		field.input.dispatchEvent(new Event('change', { bubbles: true }));
-	}
-
-	/**
-	 * Add term to field display
-	 */
-	addTermToDisplay(fieldId, id, name, path) {
-		const field = this.fields.get(fieldId);
-		if (!field || field.selectedContainer.querySelector(`[data-id="${id}"]`)) {
-			return; // Already displayed
-		}
-
-		const item = window.getTemplate('selectedTerm').cloneNode(true);
-		item.dataset.id = id;
-		item.dataset.path = path;
-		item.dataset.name = name;
-		item.dataset.taxonomy = field.taxonomy;
-		item.querySelector('span').textContent = path;
-		item.querySelector('button').title = `Remove ${name}`;
-
-		field.selectedContainer.appendChild(item);
-	}
-
-	/**
-	 * Update field from hidden input value
-	 */
-	updateFieldFromInput(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const value = field.input.value.trim();
-		field.selectedTerms.clear();
-		window.removeChildren(field.selectedContainer);
-
-		if (value !== '') {
-			const selectedIds = value.split(',')
-				.map(id => parseInt(id.trim()))
-				.filter(id => !isNaN(id));
-
-			selectedIds.forEach(id => field.selectedTerms.add(id));
-			this.initFieldDisplay(fieldId);
-		}
-	}
-
-	/**
-	 * Handle search input
-	 */
-	handleSearch(e) {
-		const query = e.target.value.trim();
-
-		// Clear existing debounce
-		if (this.searchHandler) {
-			clearTimeout(this.searchHandler);
-		}
-
-		this.searchHandler = setTimeout(() => {
-			// Single call - auto-fetches
-			this.store.setFilters({
-				search: query,
-				page: 1,
-				parent: query ? 0 : (this.store.filters.parent || 0)
-			});
-
-			window.removeChildren(this.ui.termsList);
-		}, 300);
-	}
-
-	async handleAutocomplete(e) {
-		if (!('autocomplete' in e.target.dataset)) {
+		if (this.container.open) {
+			this.showResults();
 			return;
 		}
-
-		const fieldId = this.getFieldId(e.target);
-		const field = this.fields.get(fieldId);
-
-		if (!field) return;
-
-		// Store current value immediately (fixes fast typing issue)
-		const query = e.target.value.trim();
-		field.currentAutocompleteQuery = query;
-
-		if (query.length < 2) {
-			if (field.autocompleteDropdown) {
-				field.autocompleteDropdown.hidden = true;
-			}
-			this.isAutocompleteActive = false;
-			return;
+		if (this.activeField) {
+			this.showResults(true);
 		}
+	}
 
-		this.activeField = fieldId;
-		this.isAutocompleteActive = true;
+	showResults(isAutoComplete = false) {
+		this.setLoading(false);
+		const terms = this.store.getFiltered();
+		const filters = this.store.filters;
+		const response = this.store.lastResponse?.page || {};
+		const isSearch = filters.search && filters.search.length > 0;
+		const append = filters.page > 1;
+		const field = this.currentField();
 
-		if (field.autocompleteDropdown) {
-			field.autocompleteDropdown.hidden = false;
-		}
-
-		this.store.setFilters({
-			taxonomy: field.taxonomy,
-			search: query,
-			page: 1
+		this.notify('terms-loaded', {
+			terms,
+			filters
 		});
-	}
-
-	cleanupAutocomplete(e) {
-		if (!('autocomplete' in e.target.dataset)) {
-			return;
-		}
-
-		const fieldId = this.getFieldId(e.target);
-		const field = this.fields.get(fieldId);
-
-		if (!field) return;
-
-		if (this.creator) {
-			delete this.creator;
-		}
-	}
-
-	showAutocompleteError(fieldId) {
-
-		const field = this.fields.get(fieldId);
-		if (!field) {
-			return;
-		}
-		if (!field.config.autocompleteDropdown) {
-			field.config.autocompleteDropdown = field.element.querySelector('.autocomplete-dropdown');
-		}
-		const dropdown = field.config.autocompleteDropdown;
-		if (dropdown) {
-			window.removeChildren(dropdown);
-			this.showEmptyState('Hmmm... something went wrong', dropdown);
-		}
-	}
-
-	showAutocompleteResults(field, terms, query) {
-		if (!field || !field.autocompleteDropdown) {
-			return;
-		}
-
-		const dropdown = field.autocompleteDropdown;
-		window.removeChildren(dropdown);
-
-		if (terms.length === 0) {
-			this.showEmptyState('No items found.', dropdown);
-		} else {
-			terms.forEach(term => {
-				const element = this.createAutocompleteTermElement(field, term);
-				if (element) {
-					dropdown.appendChild(element);
-				}
-			});
-		}
-
-		// Only show create button if exact match doesn't exist
-		const currentQuery = field.currentAutocompleteQuery || query;
-		if (field.canCreate && currentQuery && window.jvbTaxCreator) {
-			const exactMatch = terms.find(term =>
-				term.name.toLowerCase() === currentQuery.toLowerCase()
-			);
-
-			if (!exactMatch) {
-				const createOption = this.createNewTermOption(currentQuery);
-				dropdown.appendChild(createOption);
-			}
-		}
-
-		dropdown.hidden = false;
-	}
-
-	createNewTermOption(query) {
-		const button = document.createElement('button');
-		button.type = 'button';
-		button.className = 'autocomplete-item create-term';
-		button.dataset.query = query;
-		button.innerHTML = `<strong>Create:</strong> "${query}"`;
-
-		return button;
-	}
-
-	createAutocompleteTermElement(field, term) {
-		const item = document.createElement('button');
-		item.type = 'button';
-		item.className = 'autocomplete-item';
-		item.dataset.id = term.id;
-		item.dataset.name = term.name;
-		item.dataset.path = term.path || term.name;
-		item.textContent = term.path || term.name;
-
-		item.addEventListener('click', () => {
-			// Add term to field
-			field.selectedTerms.add(parseInt(term.id));
-			this.addTermToDisplay(field.id, term.id, term.name, term.path);
-
-			// Update input
-			field.input.value = Array.from(field.selectedTerms).join(',');
-			field.input.dispatchEvent(new Event('change', { bubbles: true }));
-
-			// Clear and hide dropdown
-			field.autocompleteDropdown.hidden = true;
-			const input = field.container.querySelector('input[data-autocomplete]');
-			if (input) input.value = '';
-		});
-
-		return item;
-	}
-
-	/**
-	 * Navigate to parent term
-	 */
-	navigateToParent() {
-		// Store handles fetch automatically
-		this.store.setFilters({
-			parent: 0,
-			page: 1
-		});
-
-		window.removeChildren(this.ui.termsList);
-		this.ui.breadcrumbs.back.hidden = true;
-	}
-
-	/**
-	 * Navigate to child term
-	 */
-	navigateToChild(termId, termName) {
-		// Store handles fetch automatically
-		this.store.setFilters({
-			parent: termId,
-			page: 1
-		});
-
-		window.removeChildren(this.ui.termsList);
-		this.updateBreadcrumbs(termId, termName);
-		this.ui.breadcrumbs.back.hidden = false;
-	}
-
-	/**
-	 * Navigate to specific path level
-	 */
-	navigateToPath(pathLevel) {
-		const parentId = parseInt(pathLevel.dataset.id) || 0;
-
-		// Store handles fetch automatically
-		this.store.setFilters({
-			parent: parentId,
-			page: 1
-		});
-
-		window.removeChildren(this.ui.termsList);
-		this.ui.breadcrumbs.back.hidden = parentId === 0;
-	}
-
-	/**
-	 * Load more terms (pagination)
-	 */
-	loadMoreTerms() {
-		const currentPage = this.store.filters.page || 1;
-		this.store.setFilter('page', currentPage + 1);
-	}
-
-	/**
-	 * Render terms list
-	 */
-	renderTerms(terms = null, append = false, showPath = false) {
-		// If no terms provided, get from store
-		if (!terms) {
-			terms = this.store.getFiltered();
-		}
-
-		if (!append) {
-			window.removeChildren(this.ui.termsList);
-		}
 
 		if (terms.length === 0) {
 			if (!append) {
-				this.showEmptyState();
+				this.showEmptyState(isSearch ? `No matching ${field.plural}.` : `No ${field.plural} available.`);
 			}
-			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);
-	}
-
-	/**
-	 * 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);
-		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();
-			}
+			this.observer.unobserve(this.ui.terms.sentinel);
 		} else {
-			this.ui.breadcrumbs.nav.appendChild(breadcrumb);
+			if (!isAutoComplete) {
+				this.showModalTerms(append, isSearch);
+
+				if (response.has_more) {
+					this.observer.observe(this.ui.terms.sentinel);
+				} else {
+					this.observer.unobserve(this.ui.terms.sentinel);
+				}
+			} else {
+				this.showAutocompleteTerms()
+			}
 		}
+
+		this.a11y.announce(terms.length, append);
+	}
+	handleFiltersChanged() {
+		// if (this.modal?.open) {
+		// 	this.setLoading();
+		// }
 	}
 
-	/**
-	 * Show loading state
-	 */
-	showLoading() {
-		this.ui.loading.loading.hidden = false;
-		this.modal.classList.add('loading');
-
-		const searchQuery = this.store?.filters?.search || '';
-		const currentParent = this.store?.filters?.parent || 0;
-
-		let message = searchQuery !== '' ?
-			`searching for "${searchQuery}" items` :
-			currentParent === 0 ?
-				'loading items' :
-				`loading child items`;
-
-		if (window.typeLoop) {
-			this.stopTyping = window.typeLoop(this.ui.loading.text, message);
-		} else {
-			this.ui.loading.text.textContent = message;
-		}
+	handleFetchError(error) {
+		this.setLoading(false);
 	}
-
-	/**
-	 * Hide loading state
-	 */
-	hideLoading() {
-		this.ui.loading.loading.hidden = true;
-		this.modal.classList.remove('loading');
-
-		if (this.stopTyping) {
-			this.stopTyping();
-		}
-	}
-
-	/**
-	 * Show empty state message
-	 */
-	showEmptyState(message = 'No items found.', container = null) {
-		if (!container) {
-			container = this.ui.termsList;
-		}
-		const emptyElement = window.getTemplate('noResults').cloneNode(true);
-
-		if (message && emptyElement.querySelector('span')) {
-			emptyElement.querySelector('span').textContent = message;
-		}
-
-		container.appendChild(emptyElement);
-	}
-
-	/**
-	 * Get field ID from any element within the field
-	 */
-	getFieldId(element) {
-		if (element.dataset.fieldId) {
-			return element.dataset.fieldId;
-		}
-
-		const fieldContainer = element.closest('[data-field-id]');
-		if (fieldContainer) {
-			return fieldContainer.dataset.fieldId;
-		}
-
-		return null;
-	}
-	/********************************************
-	BATCH FETCH: fetches first page for all taxonomies in one call
-	 ********************************************/
 	async batchFetchTaxonomies() {
-		if (this.taxonomiesToFetch.size === 0) return;
+		if (this.batchFetch.size === 0) return;
 
-		const taxonomies = Array.from(this.taxonomiesToFetch);
-		this.taxonomiesToFetch.clear();
+		const taxonomies = Array.from(this.batchFetch);
+		taxonomies.forEach(tax => this.loadedTaxonomies.add(tax));
+		this.batchFetch.clear();
 
-		// Single fetch - the data-loaded event will handle cache splitting
-		this.store.setFilters({
-			taxonomy: taxonomies.join(','),
-			page: 1,
-			search: '',
-			parent: 0
-		});
-	}
-	handleBatchDataLoaded(taxonomyString, data) {
-		const taxonomies = taxonomyString.split(',').map(t => t.trim());
-		const storeInstance = this.store.getStore(); // Access actual store instance
+		try {
+			taxonomies.forEach(tax => this.loadedTaxonomies.add(tax));
 
-		taxonomies.forEach(taxonomy => {
-			const filters = {
-				taxonomy: taxonomy,
+			await this.store.setFilters({
+				taxonomy: taxonomies.join(','),
 				page: 1,
 				search: '',
 				parent: 0
-			};
-
-			// Use the internal generateCacheKey method via store instance
-			const cacheKey = this.generateCacheKeyForFilters(filters);
-
-			// Filter items for this specific taxonomy
-			const items = Array.from(this.store.data.values())
-				.filter(item => item.taxonomy === taxonomy)
-				.map(item => item.id);
-
-			const cacheEntry = {
-				key: cacheKey,
-				items: items,
-				timestamp: Date.now(),
-				endpoint: storeInstance.config.endpoint,
-				filters: filters
-			};
-
-			// Set in both memory and IndexedDB cache
-			storeInstance.cache.set(cacheKey, cacheEntry);
-
-			// Persist to IndexedDB (if available)
-			if (storeInstance.db?.objectStoreNames.contains('cache')) {
-				const tx = storeInstance.db.transaction(['cache'], 'readwrite');
-				const objectStore = tx.objectStore('cache');
-				objectStore.put(cacheEntry);
-			}
-
-			// Update button states for this taxonomy
-			this.updateFieldsForTaxonomy(taxonomy);
-		});
-
-		// Initialize field displays
-		this.fields.forEach((config, fieldId) => {
-			if (config.selectedTerms.size > 0) {
-				this.initFieldDisplay(fieldId);
-			}
-		});
+			});
+		} catch (error) {
+			console.error('Failed to batch fetch taxonomies:', error);
+		}
 	}
 
-	/**
-	 * Generate cache key for given filters (matching DataStore's internal logic)
-	 */
-	generateCacheKeyForFilters(filters) {
-		const normalized = Object.keys(filters)
-			.sort()
-			.reduce((acc, key) => {
-				acc[key] = filters[key];
-				return acc;
-			}, {});
+	preloadTaxonomy(taxonomy) {
+		if (this.loadedTaxonomies.has(taxonomy)) return;
 
-		return JSON.stringify(normalized);
-	}
-
-	/**
-	 * Preload taxonomy data on hover
-	 */
-	async preloadTaxonomy(taxonomy) {
-		// Trigger fetch for this taxonomy
-		this.store.setFilters({
+		this.store.setFilters( {
 			taxonomy: taxonomy,
 			page: 1,
 			search: '',
 			parent: 0
 		});
-	}
-	/*****************************************
-	SUBSCRIBERS
-	 *****************************************/
 
+		this.loadedTaxonomies.add(taxonomy);
+	}
+
+	/**************************************************
+	 LOADING
+	**************************************************/
+	setLoading(on = true) {
+		this.ui.loading.loading.hidden = on;
+		this.modal.classList.toggle('loading', on);
+
+		if (on) {
+			let searchQuery = this.store.filters.search || '';
+			searchQuery = searchQuery === '' ? false : searchQuery;
+			const currentParent = this.store.filters.parent || 0;
+			const message = searchQuery
+				? `Searching for "${searchQuery} items` :
+				currentParent === 0
+					? 'loading items'
+					: 'loading child items';
+
+			if (window.typeLoop && this.ui.loading.text) {
+				this.stopTyping = window.typeLoop(this.ui.loading.text, message);
+			} else {
+				this.ui.loading.text.textContenet = message;
+			}
+		} else {
+			if (this.stopTyping) {
+				this.stopTyping();
+				this.stopTyping = null;
+			}
+		}
+	}
+	showEmptyState(message = 'No items found.', container = null) {
+		if (!container) container = this.ui.terms.list;
+		const emptyElement = window.getTemplate('noTermResults');
+		const span = emptyElement.querySelector('span');
+		if (message && span) {
+			span.textContent = message;
+		}
+		container.append(emptyElement);
+	}
+	/**************************************************
+	 SUBSCRIBERS
+	**************************************************/
 	subscribe(callback) {
 		this.subscribers.add(callback);
 		return () => this.subscribers.delete(callback);
 	}
-
-	notify(event, data = {}) {
-		this.subscribers.forEach( callback => {
+	notify(event, data={}) {
+		this.subscribers.forEach(callback => {
 			try {
 				callback(event, data);
 			} catch (error) {
@@ -1531,36 +966,27 @@
 			}
 		});
 	}
-
-	/**
-	 * Clean up
-	 */
+	/******************************************************
+	 CLEANUP
+	******************************************************/
 	destroy() {
-		// Remove event listeners
-		document.removeEventListener('click', this.handleClick);
-		document.removeEventListener('change', this.handleChange);
+		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);
 
-		// Clear intervals and cleanup
 		this.observer?.disconnect();
-
-		// Destroy all stores
-		this.store.destroy();
-
 		this.subscribers.clear();
-		// Clear all maps
 		this.fields.clear();
 		this.selectedTerms.clear();
 	}
 }
 
-/**
- * Initialize singleton
- */
 document.addEventListener('DOMContentLoaded', function() {
 	window.auth.subscribe((event) => {
 		if (event === 'auth-loaded') {
 			window.jvbSelector = new TaxonomySelector();
 		}
 	});
-
 });

--
Gitblit v1.10.0