From 56a9a1ccf764ff7a6af8f8a2292cb07443cb4aa7 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 28 May 2026 18:19:57 +0000
Subject: [PATCH] =New Gitbit setpu

---
 assets/js/concise/TaxonomySelector.js | 2124 +++++++++++++++++++++++++++++++++-------------------------
 1 files changed, 1,205 insertions(+), 919 deletions(-)

diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index b411d11..90f6076 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -1,1010 +1,1058 @@
-/**
- * TaxonomySelector - Streamlined version
- * Manages taxonomy selection fields with DataStore integration
- */
+
 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.isInitializing = true;
-		this.taxonomiesToFetch = new Set();
 		this.subscribers = new Set();
-
-		// Register DataStore
-		const store = window.jvbStore.register('taxonomies', {
-			storeName: 'terms',
-			keyPath: 'id',
-			showLoading: false,
-			indexes: [
-				{name: 'taxonomy', keyPath: 'taxonomy'},
-				{name: 'parent', keyPath: 'parent'},
-				{name: 'slug', keyPath: 'slug', unique: true},
-				{name: 'count', keyPath: 'count'},
-			],
-			endpoint: 'terms',
-			TTL: 2 * 60 * 1000,
-			filters: {
-				taxonomy: '',
-				page: 1,
-				search: '',
-				parent: 0
-			},
-			required: 'taxonomy',
-			delayFetch: true,
-		});
-		this.store = store.terms;
-
-		// Field management
 		this.fields = new Map();
-		this.selectedTerms = new Map(); // Current modal selection
+		this.selectedTerms = new Map();  // a map of fieldId => Set of selected term Ids
+		this.batchFetch = new Set();
 
-		// Modal context
 		this.activeField = null;
-		this.currentConfig = null;
-		this.disabled = false;
-
-		// Search contexts
-		this.searchContexts = new Map();
-
+		this.isInitializing = true;
+		this.lazyInit = false;
+		this.messageText = {}
 		this.init();
 	}
 
 	init() {
+		this.initStore();
+		this.initElements();
+		this.defineTemplates();
 		this.initModal();
 		this.scanExistingFields();
-		this.initGlobalListeners();
+		this.initListeners();
 
-		// Initialize creator if needed
 		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',
+				keyPath: 'id',
+				showLoading: false,
+				indexes: [
+					{name: 'taxonomy', keyPath: 'taxonomy'},
+					{name: 'parent', keyPath: 'parent'},
+					{name: 'slug', keyPath: 'slug'},
+					{name: 'count', keyPath: 'count'},
+				],
+				endpoint: 'terms',
+				TTL: 2 * 60 * 1000,
+				filters: {
+					taxonomy: '',
+					page: 1,
+					search: '',
+					parent: 0
+				},
+				required: 'taxonomy',
+				delayFetch: true,
+			}
+		);
+		this.store = store.terms;
 
 		this.store.subscribe(this.handleStoreEvent.bind(this));
-
-		this.isInitializing = false;
-		this.batchFetchTaxonomies();
 	}
 
-	needsCreator() {
-		return Array.from(this.fields.values()).some(field =>
-			field.canCreate || field.hasAutocomplete
-		);
-	}
+	defineTemplates() {
+		const T = window.jvbTemplates;
+		const terms = this;
 
-	/***********************************************************************
-	 * DATASTORE EVENT HANDLING
-	 ***********************************************************************/
+		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}`;
+			}
+		});
+		T.define('termListItem', {
+			refs: {
+				checkbox: 'input',
+				label: 'label',
+				name: 'span, .term-name'
+			},
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
 
-	handleStoreEvent(event, data) {
-		const handlers = {
-			'data-loaded': () => this.handleDataLoaded(data),
-			'filters-changed': () => this.handleFiltersChanged(data),
-			'fetch-error': () => this.handleFetchError(data.error),
-		};
+				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;
 
-		handlers[event]?.();
-	}
-
-	handleDataLoaded(data) {
-		const taxonomy = this.store.filters.taxonomy;
-
-		// Update field states for affected taxonomies
-		if (taxonomy) {
-			const taxonomies = taxonomy.includes(',')
-				? taxonomy.split(',').map(t => t.trim())
-				: [taxonomy];
-
-			taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
-		}
-
-		// Initialize displays on first load
-		if (this.isInitializing) {
-			this.fields.forEach((config, fieldId) => {
-				if (config.selectedTerms.size > 0) {
-					this.initFieldDisplay(fieldId);
+				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;
+				}
 
-		// Render based on context
-		this.renderSearchResults(data);
-	}
-
-	renderSearchResults(data) {
-		const context = this.getActiveSearchContext();
-
-		if (context === 'modal') {
-			this.renderModalResults(data);
-		} else if (context === 'autocomplete') {
-			this.renderAutocompleteResults(data);
-		}
-	}
-
-	getActiveSearchContext() {
-		if (this.modal?.open) return 'modal';
-		if (this.activeField && this.searchContexts.has(this.activeField)) {
-			return this.searchContexts.get(this.activeField);
-		}
-		return null;
-	}
-
-	renderModalResults(data) {
-		this.hideLoading();
-		const terms = this.store.getFiltered();
-		const response = this.store.lastResponse?.page || {};
-		const isSearch = data.filters?.search?.length > 0;
-		const append = response.page > 1;
-
-		this.notify('terms-loaded', { terms, filters: data.filters });
-
-		if (terms.length === 0) {
-			if (!append) {
-				this.showEmptyState(isSearch ? 'No results found.' : 'No items available.');
-			}
-			this.observer.unobserve(this.ui.sentinel);
-		} else {
-			this.renderTerms(terms, append, isSearch);
-
-			if (response.has_more) {
-				this.observer.observe(this.ui.sentinel);
-			} else {
-				this.observer.unobserve(this.ui.sentinel);
-			}
-		}
-
-		this.a11y?.announce(terms.length, append);
-	}
-
-	renderAutocompleteResults(data) {
-		const field = this.fields.get(this.activeField);
-		if (!field?.autocompleteDropdown) return;
-
-		const terms = this.store.getFiltered();
-		const query = data.filters?.search || '';
-
-		this.showAutocompleteResults(field, terms, query);
-		this.searchContexts.delete(this.activeField);
-	}
-
-	handleFiltersChanged(data) {
-		if (this.modal?.open) {
-			this.showLoading();
-		}
-	}
-
-	handleFetchError(error) {
-		this.hideLoading();
-
-		const context = this.getActiveSearchContext();
-
-		if (context === 'autocomplete') {
-			this.showAutocompleteError(this.activeField);
-			this.searchContexts.delete(this.activeField);
-		} else {
-			this.handleError(error, 'fetch');
-		}
-	}
-
-	/***********************************************************************
-	 * FIELD MANAGEMENT
-	 ***********************************************************************/
-
-	updateFieldsForTaxonomy(taxonomy) {
-		this.getFieldsForTaxonomy(taxonomy).forEach(field => {
-			this.updateFieldButtonState(field.id);
-		});
-	}
-
-	updateFieldButtonState(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const hasTerms = Array.from(this.store.data.values())
-			.some(term => term.taxonomy === field.taxonomy);
-
-		if (field.toggle) {
-			field.toggle.disabled = !hasTerms && !field.canCreate;
-			field.toggle.title = !hasTerms
-				? `No ${this.getLabel(field.taxonomy, 'single')} available`
-				: `Select ${this.getLabel(field.taxonomy, 'plural')}`;
-		}
-	}
-
-	getFieldsForTaxonomy(taxonomy) {
-		return Array.from(this.fields.values())
-			.filter(field => field.taxonomy === taxonomy);
-	}
-
-	scanExistingFields(container = document.body) {
-		container.querySelectorAll('.field.taxonomy, .field.post').forEach(selector => {
-			try {
-				this.registerField(selector);
-			} catch (error) {
-				this.handleError(error, 'scanExistingFields', selector.dataset.name);
+				if (data.hasChildren) {
+					let temp = {
+						plural: field.plural,
+						name: data.name
+					};
+					const toggle = window.jvbTemplates.create('termChildrenToggle', temp);
+					el.append(toggle);
+				}
 			}
 		});
-	}
 
-	registerField(field) {
-		const input = field.querySelector('input[type=hidden]');
-		if (!input) return false;
-
-		const fieldId = this.createFieldId(field);
-		field.dataset.fieldId = fieldId;
-
-		const button = field.querySelector('button.taxonomy-toggle');
-		const 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') || null,
-			canCreate: 'creatable' in button.dataset,
-			isRequired: 'required' in button.dataset,
-			selectedTerms: new Set(),
-			toggle: button,
-			selectedContainer: field.querySelector('.selected-items'),
-		};
-
-		// Parse initial values
-		const value = input.value.trim();
-		if (value) {
-			value.split(',')
-				.map(id => parseInt(id.trim()))
-				.filter(id => !isNaN(id))
-				.forEach(id => config.selectedTerms.add(id));
-		}
-
-		this.fields.set(fieldId, config);
-
-		// Queue for batch fetch
-		if (this.isInitializing) {
-			this.taxonomiesToFetch.add(config.taxonomy);
-		}
-
-		// Initialize display
-		if (config.selectedTerms.size > 0) {
-			this.initFieldDisplay(fieldId);
-		}
-
-		return fieldId;
-	}
-
-	createFieldId(field) {
-		this.index++;
-		return 'selector-' + this.index;
-	}
-
-	async initFieldDisplay(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field || field.selectedTerms.size === 0) return;
-
-		Array.from(field.selectedTerms).forEach(termId => {
-			const term = this.store.get(termId);
-			if (term) {
-				this.addTermDisplay(termId, term.name, term.path, 'field', fieldId);
+		T.define('termChildrenToggle', {
+			setup({el, refs, manyRefs, data}) {
+				el.ariaLabel = `View ${data.plural} nested under ${data.name}`;
 			}
 		});
-	}
 
-	/***********************************************************************
-	 * MODAL INITIALIZATION
-	 ***********************************************************************/
-
-	initModal() {
-		this.modal = document.querySelector('dialog#jvb-selector');
-		if (!this.modal) {
-			console.warn('Taxonomy selector modal not found');
-			return;
-		}
-
-		this.initModalElements();
-
-		this.modalInstance = new window.jvbModal(this.modal, {
-			handleForm: false
+		T.define('termBreadcrumb', {
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+				el.textContent = data.name;
+				el.title = data.name;
+			}
 		});
 
-		this.modalInstance.subscribe((event) => {
-			if (event === 'modal-open') this.openModal();
-			if (event === 'modal-close') this.closeModal();
+		T.define('autocompleteItem', {
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+				el.textContent = data.path||data.name;
+				el.title = `Select ${data.name}`;
+			}
 		});
-	}
 
-	initModalElements() {
-		const selectors = {
+
+	}
+	/******************************************************************
+	 ELEMENTS
+	 ******************************************************************/
+	initElements() {
+		this.selectors = {
 			search: {
-				input: '[type=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',
+				input: '[type="search"]',
+				clear: '.clear-search',
+				container: '.search-wrapper',
+				results: '.search-results',
 			},
 			create: {
-				details: '.create-new-term',
-				summary: '.create-new-term summary',
-				label: {
-					name: '[for=term_name]',
-					parent: '[for=select_parent]'
-				}
+				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',
+				},
 			}
-		};
+		}
 
-		this.ui = window.uiFromSelectors(selectors);
+		this.ui = window.uiFromSelectors(this.selectors, this.container);
+	}
 
-		// Initialize infinite scroll observer
+	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
 		});
-	}
 
-	/***********************************************************************
-	 * GLOBAL EVENT LISTENERS
-	 ***********************************************************************/
+		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);
 
-	initGlobalListeners() {
-		document.addEventListener('click', this.handleClick.bind(this));
-		document.addEventListener('change', this.handleChange.bind(this));
-		document.addEventListener('input', this.handleInput.bind(this));
-		document.addEventListener('focus', this.handleFocus.bind(this), true);
-		document.addEventListener('blur', this.handleBlur.bind(this), true);
+		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) {
-		// Toggle button
-		if (window.targetCheck(e, '.taxonomy-toggle')) {
-			e.preventDefault();
-			const fieldId = this.getFieldId(e.target);
-			const field = this.fields.get(fieldId);
-			if (field) this.setActiveField(fieldId, true);
+		if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
 			return;
 		}
-
-		// Remove selected term
-		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;
-		}
-
-		// Modal close
-		if (e.target.matches('.modal-close')) {
-			this.modalInstance?.handleClose();
-			return;
-		}
-
-		// Modal clicks
-		if (this.modal?.contains(e.target)) {
-			this.handleModalClick(e);
-		}
-	}
-
-	handleChange(e) {
-		// Hidden input changes
-		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;
-		}
-
-		// Modal checkboxes
-		if (this.modal?.contains(e.target)) {
-			this.handleModalChange(e);
-		}
-	}
-
-	handleInput(e) {
-		// Modal search
-		if (this.modal?.contains(e.target) && e.target.type === 'search') {
-			this.performSearch(e.target.value.trim(), 'modal');
-			return;
-		}
-
-		// Autocomplete
-		if ('autocomplete' in e.target.dataset) {
-			const fieldId = this.getFieldId(e.target);
-			const field = this.fields.get(fieldId);
-			if (field?.hasAutocomplete) {
-				this.performSearch(e.target.value.trim(), 'autocomplete', fieldId);
-			}
-		}
-	}
-
-	handleFocus(e) {
-		if (!('autocomplete' in e.target.dataset)) return;
-
-		const fieldId = this.getFieldId(e.target);
+		const fieldId = this.getFieldId(e.target) || this.activeField;
 		const field = this.fields.get(fieldId);
+		if (!fieldId || !field) return;
 
-		if (field?.hasAutocomplete) {
-			this.preloadTaxonomy(field.taxonomy);
+		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();
 
-	handleBlur(e) {
-		if (!('autocomplete' in e.target.dataset)) return;
-
-		setTimeout(() => {
-			const fieldId = this.getFieldId(e.target);
-			const field = this.fields.get(fieldId);
-
-			if (field?.autocompleteDropdown) {
-				field.autocompleteDropdown.hidden = true;
-			}
-
-			this.searchContexts.delete(fieldId);
-		}, 200);
+		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);
+		}
 	}
-
-	/***********************************************************************
-	 * UNIFIED SEARCH
-	 ***********************************************************************/
-
-	performSearch(query, context = 'modal', fieldId = null) {
-		const field = context === 'autocomplete'
-			? this.fields.get(fieldId)
-			: this.currentConfig;
-
+	//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;
 
-		// Autocomplete validation
-		if (context === 'autocomplete') {
-			field.currentAutocompleteQuery = query;
+		e.preventDefault();
+		e.stopPropagation();
 
-			if (query.length < 2) {
-				if (field.autocompleteDropdown) {
-					field.autocompleteDropdown.hidden = true;
-				}
-				return;
-			}
-
-			this.searchContexts.set(fieldId, 'autocomplete');
-			this.activeField = fieldId;
-
-			if (field.autocompleteDropdown) {
-				field.autocompleteDropdown.hidden = false;
-			}
+		//If it's the autocomplete field, we need to set the active field
+		if (!this.container.open) {
+			this.setField(fieldId);
 		}
 
-		// Debounced search
+		let query = e.target.value.trim();
+		this.setMessage(field,true, `Searching for "${query}" in ${field.plural??'items'}`);
 		window.debouncer.schedule(
-			`taxonomy-search-${context}-${fieldId || 'modal'}`,
+			`${fieldId}-search`,
 			async () => {
+				if (this.container.open) {
+					window.removeChildren(this.ui.terms.list);
+				}
 				await this.store.setFilters({
 					taxonomy: field.taxonomy,
 					search: query,
 					page: 1,
 					parent: query ? 0 : (this.store.filters.parent || 0)
 				});
-
-				if (context === 'modal') {
-					window.removeChildren(this.ui.termsList);
-				}
 			},
-			300
+			100
 		);
 	}
 
-	/***********************************************************************
-	 * MODAL OPERATIONS
-	 ***********************************************************************/
-
-	setActiveField(fieldId, openModal = false) {
+	setField(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) {
+			console.error('No field found...');
+			return;
+		}
 		this.activeField = fieldId;
-		this.currentConfig = this.fields.get(fieldId);
-
-		if (openModal) {
-			this.modalInstance.handleOpen();
-		}
-
-		this.store.setFilter('taxonomy', this.currentConfig.taxonomy);
-
-		// Reset modal selection state
-		this.selectedTerms.clear();
-
-		// Copy field selections to modal
-		this.currentConfig.selectedTerms.forEach(termId => {
-			const term = this.store.get(termId);
-			if (term) {
-				this.selectedTerms.set(termId, {
-					id: termId,
-					name: term.name,
-					path: term.path
-				});
-			}
-		});
+		this.setMessage(field,true, `Loading ${field.plural}...`);
+		this.resetFilters({taxonomy: field.taxonomy});
 	}
 
-	handleModalClick(e) {
-		if (window.targetCheck(e, '.remove-item')) {
-			const selectedItem = window.targetCheck(e, '.selected-item');
-			if (selectedItem) {
-				this.removeSelectedTermFromModal(selectedItem.dataset.id);
-			}
-		} else if (window.targetCheck(e, '.back-to-parent')) {
-			this.navigateToParent();
-		} else if (window.targetCheck(e, '.toggle-children')) {
-			const termItem = e.target.closest('li');
-			this.navigateToChild(
-				parseInt(termItem.dataset.id),
-				termItem.querySelector('.term-name').textContent
-			);
-		} else if (window.targetCheck(e, '.path-level')) {
-			const pathLevel = window.targetCheck(e, '.path-level');
-			this.navigateToPath(parseInt(pathLevel.dataset.id) || 0);
-		}
-	}
-
-	handleModalChange(e) {
-		if (e.target.type !== 'checkbox') return;
-
-		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);
-		}
-	}
-
-	openModal() {
-		if (!this.currentConfig) {
-			console.error('No active field set');
+	resetFilters(filters) {
+		if (!Object.hasOwn(filters, 'taxonomy')) {
 			return;
 		}
-
-		this.updateModalUI();
-		this.updateModalSelections();
-
-		window.removeChildren(this.ui.termsList);
-		this.showLoading();
+		filters = {
+			page: 1,
+			search: '',
+			parent: 0,
+			... filters
+		};
+		this.store.setFilters(filters);
 	}
 
-	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.activeField) {
-			this.saveSelectionsToField(this.activeField);
-		}
-
-		this.activeField = null;
-		this.currentConfig = null;
-	}
-
-	updateModalUI() {
-		const singular = this.getLabel(this.currentConfig.taxonomy, 'single');
-		const plural = this.getLabel(this.currentConfig.taxonomy, 'plural');
-
-		this.ui.modal.title.textContent = `Select ${plural}`;
-
-		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 ${singular}`;
-			}
-
-			if (this.ui.create.label.name) {
-				this.ui.create.label.name.textContent = `Name this ${singular}`;
-			}
-			if (this.ui.create.label.parent) {
-				this.ui.create.label.parent.textContent = `Nest it under`;
-			}
-		}
-
-		this.a11y?.announce(`Opened ${singular} selection. Choose from checkboxes or search to filter results.`);
-	}
-
-	updateModalSelections() {
-		window.removeChildren(this.ui.selectedTerms);
-
-		this.selectedTerms.forEach((termData, id) => {
-			this.addTermDisplay(id, termData.name, termData.path, 'modal');
-		});
-
-		this.checkSelectionLimits();
-	}
-
-	addSelectedTermToModal(id, name, path) {
-		this.selectedTerms.set(id, { id, name, path });
-
-		this.addTermDisplay(id, name, path, 'modal');
-		this.checkSelectionLimits();
-
-		const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
-		if (checkbox) checkbox.checked = true;
-	}
-
-	removeSelectedTermFromModal(id) {
-		this.selectedTerms.delete(parseInt(id));
-
-		const selectedItem = this.ui.selectedTerms.querySelector(`[data-id="${id}"]`);
-		if (selectedItem) selectedItem.remove();
-
-		const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
-		if (checkbox) checkbox.checked = false;
-
-		this.checkSelectionLimits();
-	}
-
-	checkSelectionLimits() {
-		if (!this.currentConfig || this.currentConfig.maxSelection === 0) {
+	handleFocus(e) {
+		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;
 
-		this.disabled = this.selectedTerms.size >= this.currentConfig.maxSelection;
+		window.debouncer.cancel(`${fieldId}-search-results`);
 
-		this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
-			if (!checkbox.checked) {
-				checkbox.disabled = this.disabled;
-			}
-		});
+		if (!this.container.open){
+			this.setField(fieldId);
+		}
 	}
 
-	saveSelectionsToField(fieldId) {
+	//Hide autocomplete dropdown on blur
+	handleBlur(e) {
+		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;
+
+		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;
 
-		field.selectedTerms.clear();
-		window.removeChildren(field.selectedContainer);
-
-		this.selectedTerms.forEach((termData, id) => {
-			field.selectedTerms.add(id);
-			this.addTermDisplay(id, termData.name, termData.path, 'field', fieldId);
-		});
-
-		field.input.value = Array.from(field.selectedTerms).join(',');
-		field.input.dispatchEvent(new Event('change', { bubbles: true }));
+		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
+		);
 	}
 
-	/***********************************************************************
-	 * TERM DISPLAY
-	 ***********************************************************************/
+	/******************************************************************
+	 MODAL
+	 ******************************************************************/
+	initModal() {
+		this.modalID = 'dialog#jvb-selector';
+		this.container = document.querySelector(this.modalID);
 
-	addTermDisplay(termId, termName, termPath, context = 'field', fieldId = null) {
-		const config = context === 'field'
-			? this.fields.get(fieldId)
-			: this.currentConfig;
+		this.modal = new window.jvbModal(
+			this.container,
+			{
+				handleForm: false,
+				open: null
+			}
+		);
+		this.modal.subscribe((event, data) => {
+			switch (event) {
+				case 'modal-close':
+					this.closeModal()
+					break;
+			}
+		});
+	}
 
-		const container = context === 'field'
-			? config.selectedContainer
-			: this.ui.selectedTerms;
+	toggleModal(fieldId, open = true) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
 
-		if (container.querySelector(`[data-id="${termId}"]`)) return;
+		if (open) {
+			this.openModal(fieldId);
+		} else {
+			this.closeModal();
+		}
+	}
 
-		const item = window.getTemplate('selectedTerm');
-		item.dataset.id = termId;
-		item.dataset.path = termPath;
-		item.dataset.name = termName;
-		item.dataset.taxonomy = config.taxonomy;
-		item.querySelector('.item-name').textContent = termPath;
-		item.querySelector('button').title = `Remove ${termName}`;
+	openModal(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
 
-		container.appendChild(item);
+		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.`;
 
-		if (context === 'modal') {
-			const checkbox = this.ui.termsList.querySelector(`input[value="${termId}"]`);
+		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());
+		}
+
+		this.setField(bulkFieldId);
+		this.ui.modal.title.textContent = `Add to ${plural}`;
+
+		if (this.ui.search?.container) {
+			this.ui.search.container.hidden = false;
+		}
+
+		window.removeChildren(this.ui.selected);
+		window.removeChildren(this.ui.terms.list);
+
+		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.activeField = null;
+
+		let message = `Closed ${field.singular} selector.`;
+		this.a11y.announce(message);
+	}
+
+	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);
+	}
+
+	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,
+			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,
+			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.batchFetch.add(config.taxonomy);
+		}
+
+		if (element.offsetParent !== null) {
+			this.updateFieldUI(fieldId);
+		} else {
+			// Defer until visible
+			requestIdleCallback(() => {
+				if (element.offsetParent !== null) {
+					this.updateFieldUI(fieldId);
+				}
+			}, {timeout: 2000});
+
+		}
+
+		return fieldId;
+	}
+
+	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);
+	}
+
+	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));
+		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);
+		});
+	}
+
+	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);
+
+		fields.forEach(field => {
+			if (!field.toggle) return;
+			field.toggle.disabled = !hasItems && !field.canCreate;
+			field.toggle.title = !hasItems
+				? `No ${field.singular} available`
+				: `Select ${field.plural}`;
+
+			field.checked = true;
+		});
+	}
+
+	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);
+			}
+			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;
+		}
+
+		this.setCreateButton(field,true);
+
+		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);
+	}
+
+	showAutocompleteTerms() {
+		const field = this.currentField();
+		if (!field || !field.hasAutocomplete || !field.ui.dropdown?.list) return;
+		const dropdown = field.ui.dropdown.list;
+		const terms = this.currentTerms();
+
+		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(()=>{});
+
+			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);
+		}
+
+		if (this.container.open) {
+			this.addTermToModal(termId);
+			const checkbox = this.ui.terms.list.querySelector(`input[value="${termId}"]`);
 			if (checkbox) checkbox.checked = true;
 		}
 	}
 
-	removeSelectedTerm(fieldId, termId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
+	updateBreadcrumbs(termId) {
+		const nav = this.ui.nav.nav;
+		if (!nav) return;
+		const existingCrumb = Array.from(nav.children)
+			.find(crumb => parseInt(crumb.dataset.id) === termId);
 
-		field.selectedTerms.delete(parseInt(termId));
-
-		const selectedItem = field.selectedContainer.querySelector(`[data-id="${termId}"]`);
-		if (selectedItem) selectedItem.remove();
-
-		field.input.value = Array.from(field.selectedTerms).join(',');
-		field.input.dispatchEvent(new Event('change', { bubbles: true }));
-	}
-
-	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) {
-			value.split(',')
-				.map(id => parseInt(id.trim()))
-				.filter(id => !isNaN(id))
-				.forEach(id => field.selectedTerms.add(id));
-
-			this.initFieldDisplay(fieldId);
-		}
-	}
-
-	/***********************************************************************
-	 * NAVIGATION
-	 ***********************************************************************/
-
-	navigateToParent() {
-		this.store.setFilters({ parent: 0, page: 1 });
-		window.removeChildren(this.ui.termsList);
-		this.ui.breadcrumbs.back.hidden = true;
-	}
-
-	navigateToChild(termId, termName) {
-		this.store.setFilters({ parent: termId, page: 1 });
-		window.removeChildren(this.ui.termsList);
-		this.updateBreadcrumbs(termId, termName);
-		this.ui.breadcrumbs.back.hidden = false;
-	}
-
-	navigateToPath(parentId) {
-		this.store.setFilters({ parent: parentId, page: 1 });
-		window.removeChildren(this.ui.termsList);
-		this.ui.breadcrumbs.back.hidden = parentId === 0;
-	}
-
-	loadMoreTerms() {
-		const currentPage = this.store.filters.page || 1;
-		this.store.setFilter('page', currentPage + 1);
-	}
-
-	updateBreadcrumbs(termId, termName) {
-		const breadcrumb = window.getTemplate('termBreadcrumb');
-		breadcrumb.dataset.id = termId;
-		breadcrumb.textContent = termName;
-		breadcrumb.title = termName;
-
-		const existingCrumb = this.ui.breadcrumbs.nav.querySelector(`[data-id="${termId}"]`);
 		if (existingCrumb) {
-			while (existingCrumb.nextElementSibling) {
-				existingCrumb.nextElementSibling.remove();
+			// Remove all siblings after this crumb
+			let nextSibling = existingCrumb.nextElementSibling;
+			while (nextSibling) {
+				const toRemove = nextSibling;
+				nextSibling = nextSibling.nextElementSibling;
+				toRemove.remove();
 			}
 		} else {
-			this.ui.breadcrumbs.nav.appendChild(breadcrumb);
+			// Add new breadcrumb
+			const term = this.store.get(termId);
+			if (!term) return;
+			const crumb = window.jvbTemplates.create('termBreadcrumb', term);
+
+			nav.append(crumb);
 		}
 	}
 
-	/***********************************************************************
-	 * RENDERING
-	 ***********************************************************************/
+	updateSelectionCount() {
+		if (!this.container.open) return;
+		const field = this.fields.get(this.activeField);
+		if (!field) return;
 
-	renderTerms(terms = null, append = false, showPath = false) {
-		if (!terms) terms = this.store.getFiltered();
+		if (this.ui.modal.count) {
+			const total = this.selectedTerms.get(this.activeField).size;
 
-		if (!append) window.removeChildren(this.ui.termsList);
-
-		if (terms.length === 0) {
-			if (!append) this.showEmptyState();
-			return;
+			this.ui.modal.count.textContent = field.limit > 0
+				? `${total} of ${field.limit} ${field.plural} selected`
+				: `${total} ${field.plural} selected`;
 		}
 
-		const currentParent = this.store.filters.parent || 0;
-		this.ui.breadcrumbs.back.hidden = currentParent === 0;
-
-		const fragment = document.createDocumentFragment();
-		terms.forEach(term => {
-			const element = this.createTermElement({
-				id: parseInt(term.id),
-				name: term.name,
-				hasChildren: term.hasChildren,
-				path: term.path || null,
-				show: showPath
-			});
-
-			if (element) fragment.appendChild(element);
-		});
-
-		this.ui.termsList.appendChild(fragment);
 	}
-
-	createTermElement(termData) {
-		if (!termData?.name) return null;
-
-		const listItem = window.getTemplate('termListItem');
-		listItem.dataset.id = termData.id;
-
-		const isSelected = this.selectedTerms.has(termData.id);
-		const checkbox = listItem.querySelector('input');
-		const label = listItem.querySelector('label');
-		const nameSpan = listItem.querySelector('.term-name');
-
-		checkbox.id = `${this.currentConfig.container.id}${termData.id}`;
-		checkbox.name = `${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`;
-		checkbox.value = termData.id;
-		checkbox.disabled = !isSelected && this.disabled;
-		checkbox.checked = isSelected;
-
-		label.htmlFor = checkbox.id;
-		label.title = termData.path || termData.name;
-		label.dataset.path = termData.path;
-
-		nameSpan.textContent = termData.show ? termData.path : termData.name;
-
-		if (termData.hasChildren) {
-			const childrenToggle = window.getTemplate('termChildrenToggle');
-			childrenToggle.ariaLabel = `View sub-terms of ${termData.name}`;
-			listItem.appendChild(childrenToggle);
+	/******************************************************************
+	 UTILITY
+	 ******************************************************************/
+	checkRendered(collection, term) {
+		if (!collection) return;
+		if (!Object.hasOwn(collection, term.taxonomy)) {
+			collection[term.taxonomy] = new Map();
 		}
-
-		return listItem;
+		return collection[term.taxonomy].has(term.id);
 	}
-
-	/***********************************************************************
-	 * AUTOCOMPLETE
-	 ***********************************************************************/
-
-	showAutocompleteResults(field, terms, query) {
-		if (!field?.autocompleteDropdown) return;
-
-		const dropdown = field.autocompleteDropdown;
-		window.removeChildren(dropdown);
-
-		if (terms.length === 0) {
-			this.showEmptyState('No items found.', dropdown);
-		} else {
-			const fragment = document.createDocumentFragment();
-
-			terms.forEach(term => {
-				const item = this.createAutocompleteItem(field, term);
-				if (item) fragment.appendChild(item);
-			});
-
-			dropdown.appendChild(fragment);
-		}
-
-		// Create button if allowed and no exact match
-		const currentQuery = field.currentAutocompleteQuery || query;
-		if (field.canCreate && currentQuery) {
-			const exactMatch = terms.find(term =>
-				term.name.toLowerCase() === currentQuery.toLowerCase()
-			);
-
-			if (!exactMatch) {
-				dropdown.appendChild(this.createAutocompleteCreateButton(currentQuery));
-			}
-		}
-
-		dropdown.hidden = false;
+	currentField() {
+		return this.fields.get(this.activeField)??false;
 	}
-
-	createAutocompleteItem(field, term) {
-		const button = document.createElement('button');
-		button.type = 'button';
-		button.className = 'autocomplete-item';
-		button.dataset.id = term.id;
-		button.dataset.name = term.name;
-		button.dataset.path = term.path || term.name;
-		button.textContent = term.path || term.name;
-
-		button.addEventListener('click', () => {
-			field.selectedTerms.add(parseInt(term.id));
-			this.addTermDisplay(term.id, term.name, term.path, 'field', field.id);
-
-			field.input.value = Array.from(field.selectedTerms).join(',');
-			field.input.dispatchEvent(new Event('change', { bubbles: true }));
-
-			field.autocompleteDropdown.hidden = true;
-			const input = field.container.querySelector('input[data-autocomplete]');
-			if (input) input.value = '';
-		});
-
-		return button;
+	currentTerms() {
+		return this.store.getFiltered();
 	}
-
-	createAutocompleteCreateButton(query) {
-		const button = document.createElement('button');
-		button.type = 'button';
-		button.className = 'autocomplete-item create-term';
-		button.dataset.query = query;
-
-		const strong = document.createElement('strong');
-		strong.textContent = 'Create: ';
-
-		button.appendChild(strong);
-		button.appendChild(document.createTextNode(`"${query}"`));
-
-		return button;
+	needsCreator() {
+		return Array.from(this.fields.values()).some(field =>
+			field.canCreate || field.hasAutocomplete
+		);
 	}
 
-	showAutocompleteError(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field?.autocompleteDropdown) return;
-
-		window.removeChildren(field.autocompleteDropdown);
-		this.showEmptyState('Hmmm... something went wrong', field.autocompleteDropdown);
-	}
-
-	/***********************************************************************
-	 * UI STATES
-	 ***********************************************************************/
-
-	showLoading() {
-		this.ui.loading.loading.hidden = false;
-		this.modal.classList.add('loading');
-
-		const searchQuery = this.store.filters.search || '';
-		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.stopTyping = window.typeLoop(this.ui.loading.text, message);
-		} else {
-			this.ui.loading.text.textContent = message;
-		}
-	}
-
-	hideLoading() {
-		this.ui.loading.loading.hidden = true;
-		this.modal.classList.remove('loading');
-
-		if (this.stopTyping) {
-			this.stopTyping();
-		}
-	}
-
-	showEmptyState(message = 'No items found.', container = null) {
-		if (!container) container = this.ui.termsList;
-
-		const emptyElement = window.getTemplate('noResults');
-		const messageSpan = emptyElement.querySelector('span');
-
-		if (message && messageSpan) {
-			messageSpan.textContent = message;
-		}
-
-		container.appendChild(emptyElement);
-	}
-
-	/***********************************************************************
-	 * UTILITIES
-	 ***********************************************************************/
-
 	getFieldId(element) {
 		if (element.dataset.fieldId) return element.dataset.fieldId;
 
@@ -1012,26 +1060,126 @@
 		return fieldContainer?.dataset.fieldId || null;
 	}
 
-	getLabel(taxonomy, type = 'single') {
-		return jvbSettings.labels[taxonomy]?.[type] || taxonomy;
-	}
-
-	async batchFetchTaxonomies() {
-		if (this.taxonomiesToFetch.size === 0) return;
-
-		const taxonomies = Array.from(this.taxonomiesToFetch);
-		this.taxonomiesToFetch.clear();
-
-		this.store.setFilters({
-			taxonomy: taxonomies.join(','),
-			page: 1,
-			search: '',
-			parent: 0
+	/**
+	 * Sets all checkbox disabled (or not)
+	 * @param {Boolean} disabled
+	 */
+	setCheckboxes(disabled) {
+		this.ui.terms.list.querySelectorAll('input[type=checkbox]').forEach(checkbox => {
+			if (!checkbox.checked) {
+				checkbox.disabled = disabled;
+			}
 		});
 	}
 
-	async preloadTaxonomy(taxonomy) {
-		await this.store.setFilters({
+	/******************************************************************
+	 DATASTORE HELPERS
+	******************************************************************/
+	handleStoreEvent(event, data) {
+		const handlers = {
+			'data-loaded': () => this.handleDataLoaded(),
+			'filters-changed': () => this.handleFiltersChanged(data),
+			'fetch-error': () => this.handleFetchError()
+		};
+
+		try {
+			handlers[event]?.(data);
+		} catch (error) {
+			console.error(`Error handling store event "${event}":`, error);
+		}
+	}
+	handleDataLoaded() {
+		const taxonomy = this.store.filters.taxonomy;
+
+		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
+		});
+
+		if (!this.activeField && isAutoComplete) {
+			return;
+		}
+		this.setMessage(this.currentField(), false);
+		if (isAutoComplete) {
+			this.showAutocompleteTerms();
+		} else {
+			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);
+		}
+
+		console.error('Store fetch error:', error);
+	}
+	async batchFetchTaxonomies() {
+		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
+			});
+		} catch (error) {
+			console.error('Failed to batch fetch taxonomies:', error);
+		}
+	}
+
+	preloadTaxonomy(taxonomy) {
+		this.store.setFilters( {
 			taxonomy: taxonomy,
 			page: 1,
 			search: '',
@@ -1039,28 +1187,133 @@
 		});
 	}
 
-	handleError(error, context, detail = null) {
-		console.error(`Taxonomy ${context} error:`, error, detail);
+	/**************************************************
+	 LOADING
+	**************************************************/
+	setCreateButton(field, show = true) {
+		if (!field.canCreate || !this.creator) return;
 
-		if (this.error?.log) {
-			this.error.log(error, {
-				component: 'TaxonomySelector',
-				action: context,
-				detail: detail
-			});
-		}
+		const conf = (this.container.open) ? this.ui : field.ui;
+		if (!conf.create?.button || !conf.create?.span) return;
 
-		if (this.modal?.open) {
-			this.showEmptyState('Error loading. Please try again.');
+		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 = {}) {
+	notify(event, data={}) {
 		this.subscribers.forEach(callback => {
 			try {
 				callback(event, data);
@@ -1069,25 +1322,58 @@
 			}
 		});
 	}
-
+	/******************************************************
+	 CLEANUP
+	******************************************************/
 	destroy() {
-		document.removeEventListener('click', this.handleClick);
-		document.removeEventListener('change', this.handleChange);
-		document.removeEventListener('input', this.handleInput);
-		document.removeEventListener('focus', this.handleFocus);
-		document.removeEventListener('blur', this.handleBlur);
+		// Cancel all debounced operations for this instance
+		this.fields.forEach((field, fieldId) => {
+			window.debouncer.cancel(`${fieldId}-search`);
+			window.debouncer.cancel(`${fieldId}-search-results`);
+		});
 
+		// Stop any typeLoop animations
+		Object.keys(this.messageText).forEach(key => {
+			if (this.messageText[key]) {
+				this.messageText[key]();
+			}
+		});
+		this.messageText = {};
+
+		// Disconnect observer
+		if (this.ui.terms?.sentinel) {
+			this.observer?.unobserve(this.ui.terms.sentinel);
+		}
 		this.observer?.disconnect();
-		this.store.destroy();
+		this.lazyObserver?.disconnect();
+
+		// 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();
 		this.fields.clear();
 		this.selectedTerms.clear();
-		this.searchContexts.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 on auth ready
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('DOMContentLoaded', function() {
 	window.auth.subscribe((event) => {
 		if (event === 'auth-loaded') {
 			window.jvbSelector = new TaxonomySelector();

--
Gitblit v1.10.0