From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh

---
 assets/js/concise/TaxonomySelector.js | 2558 +++++++++++++++++++++++++++-------------------------------
 1 files changed, 1,197 insertions(+), 1,361 deletions(-)

diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index 7f27f13..ab0e598 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -1,35 +1,54 @@
-/**
- * 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.batchFetch = new Set();
 
-		this.store = window.jvbStore.register(
+		this.activeField = null;
+		this.isInitializing = true;
+		this.lazyInit = false;
+		this.messageText = {}
+		this.init();
+	}
+
+	init() {
+		this.initStore();
+		this.initElements();
+		this.defineTemplates();
+		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: [
 					{name: 'taxonomy', keyPath: 'taxonomy'},
 					{name: 'parent', keyPath: 'parent'},
-					{name: 'slug', keyPath: 'slug', unique: true},
+					{name: 'slug', keyPath: 'slug'},
 					{name: 'count', keyPath: 'count'},
 				],
 				endpoint: 'terms',
-				TTL: 2 * 60 * 1000, //2 hours
+				TTL: 2 * 60 * 1000,
 				filters: {
 					taxonomy: '',
 					page: 1,
@@ -38,1479 +57,1266 @@
 				},
 				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];
+	defineTemplates() {
+		const T = window.jvbTemplates;
+		const terms = this;
 
-					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.');
+		T.define('emptyState');
+		T.define('selectedTerm', {
+			refs: {
+				name: '.item-name',
+				btn: 'button',
+			},
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+				el.dataset.taxonomy = data.taxonomy;
+				if (refs.name) refs.name.textContent = data.path;
+				if (refs.button) refs.button.title = `Remove ${data.name}`;
 			}
-			this.observer.unobserve(this.ui.sentinel);
-		} else {
-			this.renderTerms(terms, append, isSearch);
+		});
+		T.define('termListItem', {
+			refs: {
+				checkbox: 'input',
+				label: 'label',
+				name: 'span, .term-name'
+			},
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
 
-			// Handle pagination
-			if (response.has_more) {
-				this.observer.observe(this.ui.sentinel);
-			} else {
-				this.observer.unobserve(this.ui.sentinel);
+				let field = terms.currentField();
+				let isSelected = terms.selectedTerms.get(terms.activeField).has(data.id);
+				let limitReached = field.limit > 0 && terms.selectedTerms.get(terms.activeField).size >= field.limit;
+
+				if (refs.checkbox) {
+					refs.checkbox.dataset.id = data.id;
+					refs.checkbox.id = `${field.id}-${data.id}`;
+					refs.checkbox.name = `${field.id}-${field.taxonomy}-select`;
+					refs.checkbox.value = data.id;
+					refs.checkbox.disabled = !isSelected && limitReached;
+					refs.checkbox.checked = isSelected;
+				}
+				if (refs.label) {
+					refs.label.htmlFor = `${field.id}-${data.id}`;
+					refs.label.title = data.path??data.name;
+					refs.label.dataset.path = data.path;
+				}
+				if (refs.name) {
+					refs.name.textContent = data.show ? data.path : data.name;
+				}
+
+				if (data.hasChildren) {
+					let temp = {
+						plural: field.plural,
+						name: data.name
+					};
+					const toggle = window.jvbTemplates.create('termChildrenToggle', temp);
+					el.append(toggle);
+				}
+			}
+		});
+
+		T.define('termChildrenToggle', {
+			setup({el, refs, manyRefs, data}) {
+				el.ariaLabel = `View ${data.plural} nested under ${data.name}`;
+			}
+		});
+
+		T.define('termBreadcrumb', {
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+				el.textContent = data.name;
+				el.title = data.name;
+			}
+		});
+
+		T.define('autocompleteItem', {
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+				el.textContent = data.path||data.name;
+				el.title = `Select ${data.name}`;
+			}
+		});
+
+
+	}
+	/******************************************************************
+	 ELEMENTS
+	 ******************************************************************/
+	initElements() {
+		this.selectors = {
+			search: {
+				input: '[type="search"]',
+				clear: '.clear-search',
+				container: '.search-wrapper',
+				results: '.search-results',
+			},
+			create: {
+				button: 'button.submit-term',
+				span: '.submit-term span',
+			},
+			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',
+			},
+			message: {
+				message: 'p.message',
+				text: 'p.message span',
+			},
+			selected: '.selected-items',
+			modal: {
+				title: '#modal-title',
+				content: '.modal-content',
+				count: '.selection-count'
+			},
+			favourites: '.favourite-terms',
+			field: {
+				toggle: 'button.selector-toggle, [data-filter="taxonomy"]',
+				value: 'input[type="hidden"]',
+				selected: '.selected-items',
+				dropdown: {
+					list: '.search-results',
+					wrapper: '.auto-wrapper',
+				},
+				create: {
+					button: '.auto-wrapper .submit-term',
+					span: '.auto-wrapper button span',
+				},
+				search: 'input[data-autocomplete]',
+				message: {
+					message: 'p.message',
+					text: 'p.message span',
+				},
 			}
 		}
 
-		// Announce to screen readers
-		this.a11y?.announce(terms.length, append);
+		this.ui = window.uiFromSelectors(this.selectors, this.container);
 	}
 
-	/**
-	 * Handle fetch errors
-	 */
-	handleFetchError(error) {
-		console.error('Taxonomy fetch error:', error);
-		this.hideLoading();
+	initListeners() {
+		this.observer = new IntersectionObserver((entries) => {
+			entries.forEach(entry => {
+				if (entry.isIntersecting) {
+					this.nextPage();
+				}
+			});
+		}, {
+			root: this.ui.terms.sentinel,
+			threshold: 0.5
+		});
 
-		if (this.error?.log) {
-			this.error.log(error, {
-				component: 'TaxonomySelector',
-				action: 'fetchTerms'
-			}, () => this.fetchCurrentTerms());
+		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);
+	}
+
+	handleClick(e) {
+		if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+			return;
+		}
+		const fieldId = this.getFieldId(e.target) || this.activeField;
+		const field = this.fields.get(fieldId);
+		if (!fieldId || !field) return;
+
+		if (this.creator) {
+			let button = window.targetCheck(e, this.selectors.create.button);
+			if (button) {
+				this.maybeCreateTerm(e).then(()=>{});
+			}
+		}
+
+		const removeButton = window.targetCheck(e, '.remove-term');
+		if (removeButton) {
+			const termId = removeButton.closest('[data-id]').dataset.id??false;
+			if (fieldId && termId) {
+				this.removeSelected(parseInt(termId), fieldId);
+			}
+			return;
+		}
+
+		const autocomplete = window.targetCheck(e, '.item.autocomplete');
+
+		if (autocomplete) {
+			let termId = parseInt(autocomplete.dataset.id);
+			this.addSelected(termId, fieldId);
+			this.scheduleHideDropdown(fieldId, 6000);
+			if (field.ui.search) {
+				field.ui.search.value = '';
+			}
+			return;
+		}
+
+		const toggleButton = window.targetCheck(e, this.selectors.field.toggle);
+
+		if (toggleButton) {
+			e.preventDefault();
+			this.openModal(fieldId);
+			return;
+		}
+
+
+		if (e.target.matches('.modal-close')) {
+			this.updateFieldValue(fieldId);
+			this.modal?.handleClose();
+			return;
+		}
+
+		const backToParent = window.targetCheck(e, this.selectors.nav.back);
+		if (backToParent) {
+			this.navigateToParent();
+			return;
+		}
+
+		const toChild = window.targetCheck(e, this.selectors.nav.child);
+		if (toChild) {
+			const termItem = e.target.closest('li');
+			const termId = parseInt(termItem.dataset.id);
+
+			if (termId) {
+				this.navigateTo(termId);
+			}
+			return;
+		}
+
+		const pathLevel = window.targetCheck(e, this.selectors.nav.pathLevel);
+		if (pathLevel) {
+			const termId = parseInt(pathLevel.dataset.id)??0;
+			this.navigateTo(termId);
+			return;
+		}
+
+		const dropdown = window.targetCheck(e, this.selectors.field.dropdown);
+		if (dropdown) {
+			// reset the timer for hiding the dropdown
+			this.scheduleHideDropdown(fieldId);
+			return;
+		}
+
+		const clearSearch = window.targetCheck(e, this.selectors.search.clear);
+		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) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+			return;
+		}
+		if (!['checkbox', 'button'].includes(e.target.type)) 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.showEmptyState('Error loading terms. Please try again.');
+			this.removeSelected(termId, fieldId);
+		}
+	}
+	//For search in modal or field autocomplete
+	handleInput(e) {
+		if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+			return;
+		}
+		let fieldId = this.getFieldId(e.target)??this.activeField;
+		if (!fieldId) return;
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+		if (['checkbox', 'button'].includes(e.target.type)) return;
+
+		e.preventDefault();
+		e.stopPropagation();
+
+		//If it's the autocomplete field, we need to set the active field
+		if (!this.container.open) {
+			this.setField(fieldId);
+		}
+
+		let query = e.target.value.trim();
+		this.setMessage(field,true, `Searching for "${query}" in ${field.plural??'items'}`);
+		window.debouncer.schedule(
+			`${fieldId}-search`,
+			async () => {
+				if (this.container.open) {
+					window.removeChildren(this.ui.terms.list);
+				}
+				await this.store.setFilters({
+					taxonomy: field.taxonomy,
+					search: query,
+					page: 1,
+					parent: query ? 0 : (this.store.filters.parent || 0)
+				});
+			},
+			100
+		);
+	}
+
+	setField(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) {
+			console.error('No field found...');
+			return;
+		}
+		this.activeField = fieldId;
+		this.setMessage(field,true, `Loading ${field.plural}...`);
+		this.resetFilters({taxonomy: field.taxonomy});
+	}
+
+	resetFilters(filters) {
+		if (!Object.hasOwn(filters, 'taxonomy')) {
+			return;
+		}
+		filters = {
+			page: 1,
+			search: '',
+			parent: 0,
+			... filters
+		};
+		this.store.setFilters(filters);
+	}
+
+	handleFocus(e) {
+		if (!Object.hasOwn(e, 'target')) return;
+		if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+			return;
+		}
+		const fieldId = this.getFieldId(e.target);
+		if (!fieldId) return;
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+		if (!field.hasAutocomplete && !field.hasSearch) return;
+
+		window.debouncer.cancel(`${fieldId}-search-results`);
+
+		if (!this.container.open){
+			this.setField(fieldId);
 		}
 	}
 
+	//Hide autocomplete dropdown on blur
+	handleBlur(e) {
+		if (!Object.hasOwn(e, 'target')) return;
+		if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+			return;
+		}
+		const fieldId = this.getFieldId(e.target);
+		if (!fieldId) return;
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+		if (!field.hasAutocomplete || this.container.open) return;
+		if (e.target.closest('.remove-item')) return;
 
-	/**
-	 * Check if taxonomy has terms and update button states
-	 */
-	updateFieldButtonState(fieldId) {
+		if (e.relatedTarget && field.ui.dropdown.wrapper?.contains(e.relatedTarget)) return;
+
+		this.scheduleHideDropdown(fieldId);
+	}
+
+	scheduleHideDropdown(fieldId, delay = 1500){
 		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);
-		});
+		window.debouncer.schedule(
+			`${fieldId}-search-results`,
+			() => {
+				if (!this.container.open) {
+					this.activeField = null;
+				}
+				if (field.ui.dropdown.wrapper) {
+					field.ui.dropdown.wrapper.hidden = true;
+				}
+			},
+			delay
+		);
 	}
 
-	/**
-	 * Get fields for a specific taxonomy
-	 */
-	getFieldsForTaxonomy(taxonomy) {
-		return Array.from(this.fields.values())
-			.filter(field => field.taxonomy === taxonomy);
-	}
+	/******************************************************************
+	 MODAL
+	 ******************************************************************/
+	initModal() {
+		this.modalID = 'dialog#jvb-selector';
+		this.container = document.querySelector(this.modalID);
 
-
-
-	/**
-	 * 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
-				});
+		this.modal = new window.jvbModal(
+			this.container,
+			{
+				handleForm: false,
+				open: null
+			}
+		);
+		this.modal.subscribe((event, data) => {
+			switch (event) {
+				case 'modal-close':
+					this.closeModal()
+					break;
 			}
 		});
 	}
 
-	/**
-	 * 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;
+	toggleModal(fieldId, open = true) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
 
-		let button = (Object.hasOwn(options, 'button')) ? options.button : field.querySelector('button.taxonomy-toggle');
+		if (open) {
+			this.openModal(fieldId);
+		} else {
+			this.closeModal();
+		}
+	}
 
-		if (Object.hasOwn(options, 'buttonSelector')) {
-			this.triggers.add(options.buttonSelector);
+	openModal(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		this.setField(fieldId);
+		this.ui.modal.title.textContent = (field.isFilter) ?`Filter by ${field.singular}` : `Select ${field.plural}`;
+		if (this.ui.search.container) {
+			this.ui.search.container.hidden = !field.canSearch;
+		}
+		if (this.creator) {
+			this.creator.handleOpen(field);
+		}
+		let message = `Opened ${field.singular} selection. Choose from checkboxes, or search to filter results.`;
+
+		window.removeChildren(this.ui.selected);
+		window.removeChildren(this.ui.terms.list);
+		this.modal.handleOpen();
+
+		this.a11y.announce(message);
+	}
+
+	openEmpty(taxonomy, singular, plural, onComplete) {
+		// Store the callback for when modal closes
+		this.emptyCallback = onComplete;
+
+		// Create a temporary "field" for bulk operations
+		const bulkFieldId = `empty-${taxonomy}-${Date.now()}`;
+
+		if (!this.fields.has(bulkFieldId)) {
+			this.fields.set(bulkFieldId, {
+				id: bulkFieldId,
+				taxonomy: taxonomy,
+				singular: singular,
+				plural: plural,
+				canSearch: true,
+				canCreate:  false,
+				hasAutocomplete: false,
+				isFilter: false,
+				isEmpty: true,
+				limit: 0,
+				ui: {},
+				element: null,
+				value: null,
+				toggle: null,
+				checked: true
+			});
+			this.selectedTerms.set(bulkFieldId, new Set());
 		}
 
-		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
-		};
+		this.setField(bulkFieldId);
+		this.ui.modal.title.textContent = `Add to ${plural}`;
 
-		if (!this.hasAutocomplete && config.hasAutocomplete) {
-			this.hasAutocomplete = true;
-			this.initAutocomplete();
+		if (this.ui.search?.container) {
+			this.ui.search.container.hidden = false;
 		}
 
-		// 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));
-		}
+		window.removeChildren(this.ui.selected);
+		window.removeChildren(this.ui.terms.list);
 
-		if (Object.hasOwn(options, 'selectedItems')) {
-			options.selectedItems.forEach(id => {
-				config.selectedTerms.add(id);
+		this.modal.handleOpen();
+	}
+
+	closeModal() {
+		const field = this.fields.get(this.activeField);
+		if (!field) return;
+
+
+		this.updateFieldValue(this.activeField);
+
+		this.observer.unobserve(this.ui.terms.sentinel);
+		window.removeChildren(this.ui.terms.list);
+
+		if (field.isEmpty  && this.emptyCallback) {
+			const selectedTermIds = Array.from(this.selectedTerms.get(this.activeField) || []);
+			const selectedTerms = selectedTermIds.map(id => this.store.get(id)).filter(Boolean);
+
+			this.emptyCallback({
+				taxonomy: field.taxonomy,
+				termIds: selectedTermIds,
+				terms: selectedTerms
+			});
+
+			// Cleanup temporary bulk field
+			this.fields.delete(this.activeField);
+			this.selectedTerms.delete(this.activeField);
+			this.emptyCallback = null;
+			this.bulkAssignmentTaxonomy = null;
+		} else {
+			this.notify('selected-terms', {
+				terms: this.selectedTerms.get(this.activeField),
+				taxonomy: field.taxonomy
 			});
 		}
 
-		this.fields.set(fieldId, config);
+		this.activeField = null;
 
-		// 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;
+		let message = `Closed ${field.singular} selector.`;
+		this.a11y.announce(message);
 	}
 
-	/**
-	 * Register a filter button (simplified registration for feed blocks)
-	 */
-	registerFilterButton(button, options = {}) {
-		const fieldId = this.createFieldId(button);
-		button.dataset.fieldId = fieldId;
+	navigateToParent() {
+		const current = this.store.filters.parent;
+		if (current === 0) return;
+		let term = this.store.get(parseInt(current));
+		if (!term) {
+			this.navigateTo(0);
+			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);
+	}
 
-		if (options.buttonSelector) {
-			this.triggers.add(options.buttonSelector);
+	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 field = this.currentField();
+		if (!field) return;
+		if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
+
+		this.ui.selected.append(this.getSelectedTermUI(term));
+	}
+
+	getSelectedTermUI(term, showPath = true) {
+		return window.jvbTemplates.create('selectedTerm', term);
+	}
+	/******************************************************************
+	 FIELDS
+	 ******************************************************************/
+	scanExistingFields(container = document.body) {
+		container.querySelectorAll('[data-type="selector"], [data-field-type="selector"]').forEach(
+			selector => {
+				try {
+					if (selector.dataset.lazy) {
+						this.lazyInit = true;
+					} else {
+						// Register field if not already registered
+						// registerField will check if already registered and return early if so
+						this.registerField(selector);
+					}
+				} catch (error) {
+					this.error.log(error, {
+						component: 'TaxonomySelector',
+						action: 'scanExistingFields',
+						container: selector.dataset.name
+					});
+				}
+			}
+		);
+		if (this.lazyInit) {
+			this.initObserver(container);
+		}
+	}
+
+	unregisterFields(container) {
+		container.querySelectorAll('[data-type="selector"],[data-field-type="selector"]').forEach(
+			selector=> {
+				this.fields.delete(selector.dataset.fieldId);
+			}
+		);
+	}
+	initObserver(container){
+		this.lazyObserver = new IntersectionObserver((entries) => {
+			entries.forEach(entry => {
+				if (entry.isIntersecting && entry.target.dataset.lazy) {
+					delete entry.target.dataset.lazy;
+					this.registerField(entry.target);
+					this.lazyObserver.unobserve(entry.target);
+				}
+			});
+		}, {rootMargin: '50px'});
+
+		container.querySelectorAll('[data-type="selector"][data-lazy], [data-field-type="selector"][data-lazy]').forEach(field => {
+			this.lazyObserver.observe(field);
+		});
+	}
+
+	registerField(element, options = {}) {
+		if (element.dataset.fieldId && this.fields.has(element.dataset.fieldId)) {
+			return element.dataset.fieldId; // Already registered
+		}
+
+		let input = element.querySelector('input[type="hidden"]');
+		if (!input && !Object.hasOwn(element.dataset, 'filter')) {
+			return;
+		}
+
+		if (!('fieldId' in element.dataset)) {
+			element.dataset.fieldId = window.generateID('selector');
+		}
+		const fieldId = element.dataset.fieldId;
+
+
+		let selectors = this.selectors.field;
+		const isFilter = Object.hasOwn(element.dataset,'filter') && element.dataset.filter === 'taxonomy';
+		let button = (isFilter) ? element : element.querySelector('button.selector-toggle');
+
+		if (Object.keys(options).length === 0){
+			if (!button) return;
+			options = {
+				taxonomy: button.dataset.taxonomy,
+				single: button.dataset.single,
+				plural: button.dataset.plural,
+				search: Object.hasOwn(button.dataset, 'search'),
+				autocomplete: Object.hasOwn(button.dataset, 'autocomplete'),
+				creatable: Object.hasOwn(button.dataset, 'creatable')
+			};
+		} else if (Object.hasOwn(options, 'toggle')) {
+			button = document.querySelector(options.toggle);
+			selectors.toggle = options.toggle;
 		}
 
 		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 || []),
+			value: input,
+			element: element,
+			taxonomy: options.taxonomy??false,
+			singular: options.single??'',
+			plural: options.plural??'',
+			name: element.dataset.field,
+			canSearch: options.search??false,
+			limit: options.limit??0,
+			hasAutocomplete: options.autocomplete??false,
+			canCreate: options.creatable??false,
+			isRequired: options.required??false,
+			isFilter: isFilter,
 			toggle: button,
-			selectedContainer: options.selected || null,
-			isFilterMode: true,
-			...options
+			create: {
+				button: null,
+				span: null
+			},
+			selectors: selectors,
+			ui: window.uiFromSelectors(selectors, element),
+			checked: false,
 		};
 
+		if (isFilter && !config.ui.toggle) {
+			config.ui.toggle = element;
+		}
+		if (!config.taxonomy) {
+			console.error('TaxonomySelector: Field missing taxonomy', element);
+			return;
+		}
+		if (!config.singular || !config.plural) {
+			console.warn('TaxonomySelector: Field missing singular/plural labels', element);
+			config.singular = config.taxonomy.replace('jvb_', '');
+			config.plural = config.singular + 's';
+		}
 		this.fields.set(fieldId, config);
 
+		//Check for stored selected terms in hidden input
+		this.setSelectedFromValue(fieldId, input);
+
+
 		if (this.isInitializing) {
-			this.taxonomiesToFetch.add(config.taxonomy);
+			this.batchFetch.add(config.taxonomy);
+		}
+
+		if (element.offsetParent !== null) {
+			this.updateFieldUI(fieldId);
 		} else {
-			this.store.setFilter('taxonomy', config.taxonomy);
+			// Defer until visible
+			requestIdleCallback(() => {
+				if (element.offsetParent !== null) {
+					this.updateFieldUI(fieldId);
+				}
+			}, {timeout: 2000});
+
 		}
 
 		return fieldId;
 	}
 
-	/**
-	 * Create unique field ID
-	 */
-	createFieldId(field) {
-		this.index++;
-		return 'selector-' + this.index;
+	setSelectedFromValue(fieldId, input) {
+		if (!fieldId) return;
+		let field = this.fields.get(fieldId);
+		if (!field) return;
+		if (!input && !field.isFilter) return;
+
+		let selected = new Set();
+		if (input) {
+			input.value.trim()
+				.split(',')
+				.map(id => parseInt(id.trim()))
+				.filter(id => !isNaN(id))
+				.forEach(id => selected.add(id));
+		}
+		this.selectedTerms.set(fieldId, selected);
 	}
 
-	/**
-	 * Initialize display for a field with existing values
-	 */
-	async initFieldDisplay(fieldId) {
+	addSelected(termId, fieldId = null) {
+		if (!fieldId) fieldId = this.activeField;
+
 		const field = this.fields.get(fieldId);
-		if (!field || field.selectedTerms.size === 0) return;
+		const term = this.store.get(termId);
+		if (!field || !term) return;
 
-		const selectedIds = Array.from(field.selectedTerms);
+		const selected = this.selectedTerms.get(fieldId);
+		if (field.limit !== 0 && selected.size >= field.limit) return;
 
-		selectedIds.forEach(termId => {
-			const term = this.store.get(termId);  // Changed from getItem
-			if (term) {
-				this.addTermToDisplay(fieldId, term.id, term.name, term.path);
+		selected.add(parseInt(termId));
+		if (!this.container.open && !field.isFilter) {
+			this.updateFieldValue(fieldId);
+		}
+		this.addTermToDisplay(termId, 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) ? field.ui.selected.querySelector(`[data-id="${termId}"]`) : false;
+		if (selectedItem) selectedItem.remove();
+		if (this.container.open) {
+			let item = (this.ui.selected) ? this.ui.selected.querySelector(`[data-id="${termId}"]`) : false;
+			if (item) item.remove();
+			let checkbox = this.ui.terms.list.querySelector(`[type=checkbox][data-id="${termId}"]`);
+			if (checkbox) {
+				checkbox.checked = false;
 			}
+		}
+		if (!this.container.open && !field.isFilter) {
+			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));
+		if (field.ui.value) {
+			field.ui.value.value = selected.join(',')??'';
+			field.ui.value.dispatchEvent(new Event('change', { bubbles: true }));
+		}
+	}
+
+	checkLimits(fieldId) {
+		if (!this.container.open) return;
+		const field = this.fields.get(fieldId);
+		if (!field || !field.isFilter || field.limit === 0) return;
+		const disabled = this.selectedTerms.get(fieldId).size >= field.limit;
+		this.setCheckboxes(disabled);
+	}
+
+	updateFieldFromInput(input) {
+		const fieldId = this.getFieldId(input);
+		if (!fieldId) return;
+		const field = this.fields.get(fieldId);
+		if(!field) return;
+
+		this.setSelectedFromValue(fieldId, input);
+		this.updateFieldUI(fieldId);
+	}
+
+	updateFieldUI(fieldId) {
+		const field = this.fields.get(fieldId);
+		let selected = this.selectedTerms.get(fieldId)??new Set();
+		if (!field || field.isFilter || selected.size === 0) return;
+
+		Array.from(selected).forEach(termId => {
+			this.addTermToDisplay(termId, fieldId);
 		});
 	}
 
-	/**
-	 * Initialize modal elements
-	 */
-	initModal() {
-		this.modalID = 'dialog#jvb-selector';
-		this.modal = document.querySelector(this.modalID);
+	updateFieldsForTaxonomy(taxonomy) {
+		let fields = Array.from(this.fields.values())
+			.filter(field => field.taxonomy === taxonomy);
+		const hasItems = Array.from(this.store.data.values())
+			.some(term => term && term.taxonomy === taxonomy);
 
-		if (!this.modal) {
-			console.warn('Taxonomy selector modal not found');
-			return;
-		}
+		fields.forEach(field => {
+			if (!field.toggle) return;
+			field.toggle.disabled = !hasItems && !field.canCreate;
+			field.toggle.title = !hasItems
+				? `No ${field.singular} available`
+				: `Select ${field.plural}`;
 
-		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;
-			}
+			field.checked = true;
 		});
 	}
 
-	/**
-	 * Initialize modal element references
-	 */
-	initModalElements() {
-		this.selectors = {
-			search: {
-				input: '[type=search]',
-				clear: '.clear-search',
-				container: '.search-wrapper'
-			},
-			termsList: 	'.items-container',
-			termsWrap:	'.items-wrap',
-			breadcrumbs: {
-				nav: 'nav.term-navigation',
-				back: '.back-to-parent',
-			},
-			loading: {
-				loading: '.loading',
-				text: '.loading span'
-			},
-			selectedTerms: '.selected-items',
-			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);
-
-		// Initialize intersection observer for infinite scroll
-		this.observer = new IntersectionObserver((entries) => {
-			entries.forEach(entry => {
-				if (entry.isIntersecting) {
-					this.loadMoreTerms();
-				}
-			});
-		}, {
-			root: this.ui.termsWrap,
-			threshold: 0.5
-		});
-	}
-
-	/**
-	 * 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();
-		}
-	}
-
-	initAutocomplete()
-	{
-		this.autocompleteHandler = window.debounce((e) => 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;
+	showModalTerms(showPath = false) {
+		const field = this.currentField();
+		const terms = this.store.getFiltered();
+		if (terms.length === 0) {
+			if (this.store.filters.page??1 === 1) {
+				window.removeChildren(this.ui.terms.list);
 			}
-
-			const fieldId = this.getFieldId(e.target);
-			const field = this.fields.get(fieldId);
-
-			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));
-
-		if (toggleButton) {
-			e.preventDefault();
-			this.handleToggleClick(toggleButton);
-			return;
-		}
-
-		// Handle remove selected term buttons
-		const removeButton = window.targetCheck(e, 'button.remove-item');
-		if (removeButton && e.target.closest('.jvb-selector')) {
-			const fieldId = this.getFieldId(removeButton);
-			const termId = removeButton.closest('.selected-item').dataset.id;
-			this.removeSelectedTerm(fieldId, termId);
-			return;
-		}
-
-		// Handle modal close button
-		if (e.target.matches('.modal-close')) {
-			if (this.modalInstance) {
-				this.modalInstance.handleClose();
+			this.setMessage(field,true, this.store.filters.search === ''
+				? `No matching ${field.plural}.`
+				: `No ${field.plural} found.`,
+				false);
+			if (this.ui.terms.sentinel) {
+				this.observer.unobserve(this.ui.terms.sentinel);
 			}
 			return;
 		}
 
-		// Handle clicks within the modal
-		if (this.modal && this.modal.contains(e.target)) {
-			this.handleModalClick(e);
-		}
-	}
+		this.setCreateButton(field,true);
 
-	/**
-	 * 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'
-				});
+		if (this.ui.terms.sentinel) {
+			if (this.store.lastResponse?.has_more) {
+				this.observer.observe(this.ui.terms.sentinel);
+			} else {
+				this.observer.unobserve(this.ui.terms.sentinel);
 			}
 		}
+
+		const currentParent = this.store.filters.parent??0;
+		this.ui.nav.back.hidden = currentParent === 0;
+
+		window.chunkIt(
+			terms,
+			(term) => this.createTermElement({show:showPath, ... term}),
+			(fragment) => this.ui.terms.list.append(fragment),
+			10
+		).then(()=>{});
+
+		if (terms.length > 0) {
+			this.setMessage(field,false);
+		}
+	}
+	createTermElement(term) {
+		if (!term || !term.name) return null;
+		return window.jvbTemplates.create('termListItem', term);
 	}
 
-	/**
-	 * Set the active field for modal operations
-	 */
-	setActiveField(fieldId, openModal = false) {
-		this.activeField = fieldId;
-		this.currentConfig = this.fields.get(fieldId);
+	showAutocompleteTerms() {
+		const field = this.currentField();
+		if (!field || !field.hasAutocomplete || !field.ui.dropdown?.list) return;
+		const dropdown = field.ui.dropdown.list;
+		const terms = this.currentTerms();
 
-		this.currentSingular = this.getSingular(this.currentConfig.taxonomy);
-		this.currentPlural = this.getPlural(this.currentConfig.taxonomy);
+		window.removeChildren(dropdown);
+		if (terms.length === 0) {
+			this.setMessage(field,true, `No ${field.plural} found.`, false);
+		} else {
+			window.chunkIt(
+				terms,
+				(term) => this.createAutocompleteTerm(term),
+				(fragment) => dropdown.append(fragment)
+			).then(()=>{});
 
-		if (openModal) {
-			this.modalInstance.handleOpen();
+			this.setMessage(field,false);
+		}
+		this.setCreateButton(field,true);
+
+		if (field.ui.dropdown.wrapper) {
+			field.ui.dropdown.wrapper.hidden = false;
+		}
+	}
+
+	createAutocompleteTerm(term) {
+		return window.jvbTemplates.create('autocompleteItem', term);
+	}
+	/******************************************************************
+	 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 && field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
+
+
+		let item = this.getSelectedTermUI(term);
+
+		if (field.ui.selected) {
+			field.ui.selected.append(item);
 		}
 
-		// Set taxonomy filter - store handles the rest
-		this.store.setFilter('taxonomy', this.currentConfig.taxonomy);
+		if (this.container.open) {
+			this.addTermToModal(termId);
+			const checkbox = this.ui.terms.list.querySelector(`input[value="${termId}"]`);
+			if (checkbox) checkbox.checked = true;
+		}
+	}
 
-		// Clear modal selection state
-		this.selectedTerms.clear();
+	updateBreadcrumbs(termId) {
+		const nav = this.ui.nav.nav;
+		if (!nav) return;
+		const existingCrumb = Array.from(nav.children)
+			.find(crumb => parseInt(crumb.dataset.id) === termId);
 
-		// Copy field's current selections to modal state
-		this.currentConfig.selectedTerms.forEach(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) {
-				this.selectedTerms.set(termId, {
-					id: termId,
-					name: term.name,
-					path: term.path
-				});
-			}
-		});
-	}
+			if (!term) return;
+			const crumb = window.jvbTemplates.create('termBreadcrumb', term);
 
-
-	/**
-	 * Handle clicks within modal
-	 */
-	handleModalClick(e) {
-		if (window.targetCheck(e, '.remove-item')) {
-			let selectedItem = window.targetCheck(e, '.selected-item');
-			if (selectedItem) {
-				this.removeSelectedTermFromModal(selectedItem.dataset.id);
-			}
-		} else if (window.targetCheck(e, '.back-to-parent')) {
-			this.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);
+			nav.append(crumb);
 		}
 	}
 
-	/**
-	 * 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);
-		}
-
-		// Update modal UI
-		this.updateModalForTaxonomy();
-
-		// Load selected terms display
-		this.updateModalSelections();
-		this.updateSelectionCount();
-
-		// Clear terms list and show loading
-		window.removeChildren(this.ui.termsList);
-		this.showLoading();
-	}
-
-	/**
-	 * Update selection count display in modal
-	 */
 	updateSelectionCount() {
-		if (!this.currentConfig) return;
+		if (!this.container.open) return;
+		const field = this.fields.get(this.activeField);
+		if (!field) return;
 
-		const count = this.selectedTerms.size;
-		const max = this.currentConfig.maxSelection;
+		if (this.ui.modal.count) {
+			const total = this.selectedTerms.get(this.activeField).size;
 
-		// 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`;
-			}
-		}
-	}
-
-
-
-	/**
-	 * Get singular label for taxonomy
-	 */
-	getSingular(taxonomy) {
-		return jvbSettings.labels[taxonomy]?.single || taxonomy;
-	}
-
-	/**
-	 * Get plural label for taxonomy
-	 */
-	getPlural(taxonomy) {
-		return jvbSettings.labels[taxonomy]?.plural || taxonomy;
-	}
-
-	/**
-	 * Close modal and save selections
-	 */
-	closeModal() {
-		this.observer.unobserve(this.ui.sentinel);
-		window.removeChildren(this.ui.termsList);
-
-		this.notify('selected-terms', {
-			terms: this.selectedTerms,
-			taxonomy: this.currentConfig.taxonomy
-		});
-
-		if (this.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);
+			this.ui.modal.count.textContent = field.limit > 0
+				? `${total} of ${field.limit} ${field.plural} selected`
+				: `${total} ${field.plural} selected`;
 		}
 
-		// Cleanup
-		if (this.currentConfig?.canSearch && this.searchHandler) {
-			this.ui.search.input.removeEventListener('input', this.searchHandler);
+	}
+	/******************************************************************
+	 UTILITY
+	 ******************************************************************/
+	checkRendered(collection, term) {
+		if (!collection) return;
+		if (!Object.hasOwn(collection, term.taxonomy)) {
+			collection[term.taxonomy] = new Map();
 		}
+		return collection[term.taxonomy].has(term.id);
+	}
+	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
+		);
+	}
 
-		if (!this.hasAutocomplete && this.creator) {
-			delete this.creator;
-		}
+	getFieldId(element) {
+		if (element.dataset.fieldId) return element.dataset.fieldId;
 
-		// Remove: this.activeStore = null;
-		this.activeField = null;
-		this.currentConfig = null;
+		const fieldContainer = element.closest('[data-field-id]');
+		return fieldContainer?.dataset.fieldId || null;
 	}
 
 	/**
-	 * Reset modal state
-	 */
-	resetModalState() {
-		this.disabled = false;
-
-		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}`;
-
-		if (this.ui.search.container) {
-			this.ui.search.container.style.display = this.currentConfig.canSearch ? 'block' : 'none';
-		}
-
-		if (this.ui.create.details) {
-			this.ui.create.details.style.display = this.currentConfig.canCreate ? 'block' : 'none';
-			this.ui.create.details.hidden = !this.currentConfig.canCreate;
-
-			if (this.ui.create.summary) {
-				this.ui.create.summary.textContent = `Add new ${this.currentSingular}`;
-			}
-
-			if (this.ui.create.label.name) {
-				this.ui.create.label.name.textContent = `Name this ${this.currentSingular}`;
-			}
-			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);
-	}
-
-	/**
-	 * Update modal selections display
-	 */
-	updateModalSelections() {
-		window.removeChildren(this.ui.selectedTerms);
-
-		this.selectedTerms.forEach((termData, id) => {
-			this.addTermToModalDisplay(id, termData.name, termData.path);
-		});
-
-		this.checkSelectionLimits();
-	}
-
-	/**
-	 * Add selected term to modal
-	 */
-	addSelectedTermToModal(id, name, path) {
-		this.selectedTerms.set(id, {
-			id: id,
-			name: name,
-			path: path
-		});
-
-		this.addTermToModalDisplay(id, name, path);
-		this.checkSelectionLimits();
-
-		// Check the corresponding checkbox
-		const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
-		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();
-		}
-
-		// Uncheck the corresponding checkbox
-		const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
-		if (checkbox) {
-			checkbox.checked = false;
-		}
-
-		this.checkSelectionLimits();
-	}
-
-	/**
-	 * Add term to modal display
-	 */
-	addTermToModalDisplay(id, name, path) {
-		const item = window.getTemplate('selectedTerm').cloneNode(true);
-		item.dataset.id = id;
-		item.dataset.path = path;
-		item.dataset.name = name;
-		item.dataset.taxonomy = this.currentConfig.taxonomy;
-		item.querySelector('span').textContent = path;
-		item.querySelector('button').title = `Remove ${name}`;
-
-		this.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
+	 * 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(data),
+			'fetch-error': () => this.handleFetchError()
+		};
 
-		// Clear current field selections
-		field.selectedTerms.clear();
-		window.removeChildren(field.selectedContainer);
+		try {
+			handlers[event]?.(data);
+		} catch (error) {
+			console.error(`Error handling store event "${event}":`, error);
+		}
+	}
+	handleDataLoaded() {
+		const taxonomy = this.store.filters.taxonomy;
 
-		// Add modal selections to field
-		this.selectedTerms.forEach((termData, id) => {
-			field.selectedTerms.add(id);
-			this.addTermToDisplay(fieldId, id, termData.name, termData.path);
+		if (taxonomy) {
+			const taxonomies = taxonomy.split(',').map(t => t.trim());
+			taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
+		}
+
+		if (this.container.open) {
+			this.showResults();
+			return;
+		}
+		if (this.activeField) {
+			this.showResults(true);
+			return;
+		}
+	}
+
+	showResults(isAutoComplete = false) {
+		const terms = this.store.getFiltered();
+		const filters = this.store.filters;
+		const isSearch = filters.search && filters.search.length > 0;
+
+		this.notify('terms-loaded', {
+			terms,
+			filters
 		});
 
-		// Update hidden input
-		const selectedIds = Array.from(field.selectedTerms);
-		field.input.value = selectedIds.join(',');
-		field.input.dispatchEvent(new Event('change', { bubbles: true }));
-	}
-
-	/**
-	 * Remove selected term from field
-	 */
-	removeSelectedTerm(fieldId, termId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const id = parseInt(termId);
-		field.selectedTerms.delete(id);
-
-		// Remove from display
-		const selectedItem = field.selectedContainer.querySelector(`[data-id="${id}"]`);
-		if (selectedItem) {
-			selectedItem.remove();
-		}
-
-		// Update hidden input
-		const selectedIds = Array.from(field.selectedTerms);
-		field.input.value = selectedIds.join(',');
-		field.input.dispatchEvent(new Event('change', { bubbles: true }));
-	}
-
-	/**
-	 * Add term to field display
-	 */
-	addTermToDisplay(fieldId, id, name, path) {
-		const field = this.fields.get(fieldId);
-		if (!field || field.selectedContainer.querySelector(`[data-id="${id}"]`)) {
-			return; // Already displayed
-		}
-
-		const item = window.getTemplate('selectedTerm').cloneNode(true);
-		item.dataset.id = id;
-		item.dataset.path = path;
-		item.dataset.name = name;
-		item.dataset.taxonomy = field.taxonomy;
-		item.querySelector('span').textContent = path;
-		item.querySelector('button').title = `Remove ${name}`;
-
-		field.selectedContainer.appendChild(item);
-	}
-
-	/**
-	 * Update field from hidden input value
-	 */
-	updateFieldFromInput(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const value = field.input.value.trim();
-		field.selectedTerms.clear();
-		window.removeChildren(field.selectedContainer);
-
-		if (value !== '') {
-			const selectedIds = value.split(',')
-				.map(id => parseInt(id.trim()))
-				.filter(id => !isNaN(id));
-
-			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.activeField && isAutoComplete) {
 			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;
-		}
-
-		this.activeField = fieldId;
-		this.isAutocompleteActive = true;
-
-		if (field.autocompleteDropdown) {
-			field.autocompleteDropdown.hidden = false;
-		}
-
-		this.store.setFilters({
-			taxonomy: field.taxonomy,
-			search: query,
-			page: 1
-		});
-	}
-
-	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);
+		this.setMessage(this.currentField(), false);
+		if (isAutoComplete) {
+			this.showAutocompleteTerms();
 		} else {
-			terms.forEach(term => {
-				const element = this.createAutocompleteTermElement(field, term);
-				if (element) {
-					dropdown.appendChild(element);
+			this.showModalTerms(isSearch);
+		}
+
+
+		this.a11y.announce(terms.length);
+	}
+	handleFiltersChanged(data) {
+		//maybe do something?
+	}
+
+	handleFetchError(error) {
+		const field = this.currentField();
+		this.setMessage(field, true, 'Something went wrong.', false);
+
+		const conf = this.container.open || field?.isFilter ? this.ui : field?.ui;
+		const p = conf?.message?.message;
+
+		if (p && !p.querySelector('.clear-cache-btn')) {
+			const btn = document.createElement('button');
+			btn.className = 'clear-cache-btn';
+			btn.type = 'button';
+			btn.textContent = 'Clear cache and try again';
+			btn.addEventListener('click', async () => {
+				btn.remove();
+				this.store.clearCache();
+				if (this.activeField && field) {
+					await this.store.setFilters({
+						taxonomy: field.taxonomy,
+						page: 1,
+						search: '',
+						parent: 0
+					});
 				}
 			});
+			p.appendChild(btn);
 		}
 
-		// Use stored current query instead of debounced one
-		const currentQuery = field.currentAutocompleteQuery || query;
-		if (field.canCreate && currentQuery && window.jvbTaxCreator) {
-			const createOption = this.createNewTermOption(currentQuery);
-			dropdown.appendChild(createOption);
-		}
-
-		dropdown.hidden = false;
+		console.error('Store fetch error:', error);
 	}
-
-	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();
-			}
-			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();
-			}
-		} else {
-			this.ui.breadcrumbs.nav.appendChild(breadcrumb);
-		}
-	}
-
-	/**
-	 * 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;
-		}
-	}
-
-	/**
-	 * 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;
-
-		const taxonomies = Array.from(this.taxonomiesToFetch);
-		this.taxonomiesToFetch.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
-
-		taxonomies.forEach(taxonomy => {
-			const filters = {
-				taxonomy: taxonomy,
+		if (this.batchFetch.size === 0) return;
+		const taxonomies = Array.from(this.batchFetch);
+		this.batchFetch.clear();
+		try {
+			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;
-			}, {});
-
-		return JSON.stringify(normalized);
-	}
-
-	/**
-	 * Preload taxonomy data on hover
-	 */
-	async preloadTaxonomy(taxonomy) {
-		// Trigger fetch for this taxonomy
-		this.store.setFilters({
+	preloadTaxonomy(taxonomy) {
+		this.store.setFilters( {
 			taxonomy: taxonomy,
 			page: 1,
 			search: '',
 			parent: 0
 		});
 	}
-	/*****************************************
-	SUBSCRIBERS
-	 *****************************************/
 
+	/**************************************************
+	 LOADING
+	**************************************************/
+	setCreateButton(field, show = true) {
+		if (!field.canCreate || !this.creator) return;
+
+		const conf = (this.container.open) ? this.ui : field.ui;
+		if (!conf.create?.button || !conf.create?.span) return;
+
+		const createButton = conf.create.button;
+		createButton.hidden = !show;
+		const buttonSpan = conf.create.span;
+		const input = (this.container.open) ? conf.search.input : conf.search;
+		if (!input) return;
+
+		let results = this.currentTerms()??[];
+		let matches = results.map(t => t.name);
+
+		let query = input.value;
+		const willShow = show && query.length >= 2 && !matches.includes(query);
+		createButton.hidden = !willShow;
+		if (willShow) {
+			buttonSpan.textContent = input.value??'';
+		}
+	}
+	async maybeCreateTerm(e) {
+		const field = this.currentField();
+		if (!field) return;
+
+		window.debouncer.cancel(`${field.id}-search-results`);
+
+		let data = {
+			taxonomy: field.taxonomy,
+			parent: this.store.filters.parent??0
+		}
+
+		if (!this.container.open || this.ui.search.input.value !== '') {
+			data.name = (this.container.open) ? this.ui.search.input.value : field.ui.search.value;
+		} else {
+			data.parent = this.creator.ui.parent.value??data.parent;
+			data.name = this.creator.ui.name.value??false;
+		}
+
+		if (data.parent !== undefined && data.name) {
+			this.setMessage(field,true, `Creating "${data.name}"...`);
+			this.setCreateButton(field,false);
+
+			if (this.container.open) {
+				window.removeChildren(this.ui.terms.list);
+			} else {
+				field.ui.search.disabled = true;
+				if (field.ui.dropdown.wrapper) {
+					field.ui.dropdown.wrapper.hidden = false;
+				}
+			}
+
+			let term = await this.creator.handleTermCreation(data);
+
+			if (term) {
+				// Stop any typeLoop animation and show success message WITHOUT typeLoop
+				this.setMessage(field,true, `"${term.name}" created!`, false);
+
+				this.addSelected(term.id, field.id);
+				this.updateFieldValue(field.id);
+				// For autocomplete, show the newly created term in dropdown
+				if (!this.container.open && field.ui.dropdown.list) {
+					window.removeChildren(field.ui.dropdown.list);
+					const termElement = this.createAutocompleteTerm(term);
+					if (termElement) {
+						termElement.classList.add('newly-created');
+						field.ui.dropdown.list.append(termElement);
+					}
+				}
+				this.scheduleHideDropdown(field.id, 300);
+				this.setMessage(field,false);
+			} else {
+				// Creation failed - hide immediately
+				this.setMessage(field,false);
+				if (!this.container.open && field.ui.dropdown.wrapper) {
+					field.ui.dropdown.wrapper.hidden = true;
+				}
+			}
+
+			if (!this.container.open) {
+				field.ui.search.disabled = false;
+				field.ui.search.value = '';
+			}
+		}
+	}
+	setMessage(field, show = true, message = '', type = true) {
+		const conf = this.container.open||field.isFilter ? this.ui : (field.isFilter ? null : field.ui);
+		if (!conf?.message?.message) return;
+
+		message = (message === '') ? `No ${field.plural??'items'} found.` : message;
+
+		const p = conf.message.message;
+		const pText = conf.message.text;
+
+		p.hidden = !show;
+		if (show) {
+			if (message && pText) {
+				if (type && window.typeLoop && pText) {
+					if (this.messageText[field.id]) {
+						this.messageText[field.id]();
+						delete this.messageText[field.id];
+					}
+					this.messageText[field.id] = window.typeLoop(pText, message);
+				} else {
+					pText.textContent = message;
+				}
+
+			}
+		} else {
+			if (this.messageText[field.id]) {
+				this.messageText[field.id]();
+				delete this.messageText[field.id];
+			}
+		}
+	}
+	/**************************************************
+	 SUBSCRIBERS
+	**************************************************/
 	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) {
@@ -1518,31 +1324,61 @@
 			}
 		});
 	}
-
-	/**
-	 * Clean up
-	 */
+	/******************************************************
+	 CLEANUP
+	******************************************************/
 	destroy() {
-		// Remove event listeners
-		document.removeEventListener('click', this.handleClick);
-		document.removeEventListener('change', this.handleChange);
+		// Cancel all debounced operations for this instance
+		this.fields.forEach((field, fieldId) => {
+			window.debouncer.cancel(`${fieldId}-search`);
+			window.debouncer.cancel(`${fieldId}-search-results`);
+		});
 
-		// Clear intervals and cleanup
+		// Stop any typeLoop animations
+		Object.keys(this.messageText).forEach(key => {
+			if (this.messageText[key]) {
+				this.messageText[key]();
+			}
+		});
+		this.messageText = {};
+
+		// Disconnect observer
+		if (this.ui.terms?.sentinel) {
+			this.observer?.unobserve(this.ui.terms.sentinel);
+		}
 		this.observer?.disconnect();
+		this.lazyObserver?.disconnect();
 
-		// Destroy all stores
-		this.store.destroy();
+		// Remove event listeners
+		document.removeEventListener('click', this.clickHandler);
+		document.removeEventListener('change', this.changeHandler);
+		document.removeEventListener('input', this.inputHandler);
+		document.removeEventListener('focus', this.focusHandler, true);
+		document.removeEventListener('blur', this.blurHandler, true);
 
+		// Clear data structures
 		this.subscribers.clear();
-		// Clear all maps
 		this.fields.clear();
 		this.selectedTerms.clear();
+		this.batchFetch.clear();
+
+		// Cleanup creator if exists
+		if (this.creator) {
+			this.creator.destroy();
+			this.creator = null;
+		}
+
+		// Unsubscribe from store
+		if (this.store) {
+			this.store = null;
+		}
 	}
 }
 
-/**
- * Initialize singleton
- */
 document.addEventListener('DOMContentLoaded', function() {
-	window.jvbSelector = new TaxonomySelector();
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbSelector = new TaxonomySelector();
+		}
+	});
 });

--
Gitblit v1.10.0