From a504a49004671287d9e0c78fd107c4abf669cc2a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 08 Jan 2026 16:52:14 +0000
Subject: [PATCH] Merge branch 'main' of https://github.com/jakevdwerf/jvb

---
 assets/js/concise/TaxonomySelector.js |  565 +++++++++++++++++++++++++++++++++++++------------------
 1 files changed, 379 insertions(+), 186 deletions(-)

diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index fcd9364..1d84d93 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -1,3 +1,4 @@
+
 class TaxonomySelector {
 	constructor() {
 		this.container = document.querySelector('dialog#jvb-selector');
@@ -9,11 +10,11 @@
 		this.subscribers = new Set();
 		this.fields = new Map();
 		this.selectedTerms = new Map();  // a map of fieldId => Set of selected term Ids
-		this.loadedTaxonomies = new Set(); // a set of taxonomies, to know whether we should preload a newly registered field
 		this.batchFetch = new Set();
 
 		this.activeField = null;
 		this.isInitializing = true;
+		this.messageText = {}
 		this.init();
 	}
 
@@ -67,10 +68,14 @@
 	initElements() {
 		this.selectors = {
 			search: {
-				input: '[type=search]',
+				input: '[type="search"]',
 				clear: '.clear-search',
 				container: '.search-wrapper',
-				results: '.search-results'
+				results: '.search-results',
+			},
+			create: {
+				button: 'button.submit-term',
+				span: '.submit-term span',
 			},
 			terms: {
 				list: '.items-container',
@@ -83,9 +88,9 @@
 				child: '.toggle-children',
 				pathLevel: '.path-level',
 			},
-			loading: {
-				loading: '.loading',
-				text: '.loading span',
+			message: {
+				message: 'p.message',
+				text: 'p.message span',
 			},
 			selected: '.selected-items',
 			modal: {
@@ -95,15 +100,26 @@
 			},
 			favourites: '.favourite-terms',
 			field: {
-				toggle: 'button.taxonomy-toggle',
+				toggle: 'button.taxonomy-toggle, [data-filter="taxonomy"]',
 				value: 'input[type="hidden"]',
 				selected: '.selected-items',
-				dropdown: '.search-results',
-				search: '[data-autocomplete]',
+				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(this.selectors);
+		this.ui = window.uiFromSelectors(this.selectors, this.container);
 	}
 
 	initListeners() {
@@ -132,41 +148,40 @@
 	}
 
 	handleClick(e) {
-		const fieldId = this.getFieldId(e.target);
+		const fieldId = this.getFieldId(e.target) || this.activeField;
 		const field = this.fields.get(fieldId);
 		if (!fieldId || !field) return;
 
-		const autoComplete = window.targetCheck(e, '[data-autocomplete-select]');
+		const autoComplete = window.targetCheck(e, '.item.autocomplete');
+
 		if (autoComplete) {
 			let termId = parseInt(autoComplete.dataset.id);
 			this.addSelected(termId, fieldId);
-			if (field.ui.dropdown) {
-				field.ui.dropdown.hidden = true;
-			}
-
+			this.scheduleHideDropdown(fieldId);
 			if (field.ui.search) {
 				field.ui.search.value = '';
 			}
 		}
 
-		const toggleButton = window.targetCheck(e, field.ui.toggle);
+		const toggleButton = window.targetCheck(e, this.selectors.field.toggle);
+
 		if (toggleButton) {
 			e.preventDefault();
 			this.openModal(fieldId);
 			return;
 		}
 
-		const removeButton = window.targetCheck(e, 'button.remove-item');
+		const removeButton = window.targetCheck(e, '.remove-term');
 		if (removeButton) {
-			const fieldId = this.getFieldId(removeButton);
-			const termId = removeButton.closest('.selected-item').dataset.id??false;
+			const termId = removeButton.closest('[data-id]').dataset.id??false;
 			if (fieldId && termId) {
-				this.removeSelected(termId, fieldId);
+				this.removeSelected(parseInt(termId), fieldId);
 			}
 			return;
 		}
 
 		if (e.target.matches('.modal-close')) {
+			this.updateFieldValue(fieldId);
 			this.modal?.handleClose();
 			return;
 		}
@@ -194,11 +209,10 @@
 			this.navigateTo(termId);
 		}
 
-		const dropdown = window.targetCheck(e, field.selectors.dropdown);
+		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);
@@ -217,12 +231,19 @@
 			}
 		}
 
+		if (this.creator) {
+			let button = window.targetCheck(e, this.selectors.create.button);
+			if (button) {
+				this.maybeCreateTerm(e).then(()=>{});
+			}
+		}
+
 	}
 	handleChange(e) {
 		if (!this.container.contains(e.target)) {
 			return;
 		}
-		if (e.target.type !== 'checkbox') return;
+		if (!['checkbox', 'button'].includes(e.target.type)) return;
 		e.preventDefault();
 		e.stopPropagation();
 
@@ -240,29 +261,59 @@
 		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.activeField = fieldId;
+			this.setField(fieldId);
 		}
 
-		const query = e.target.value.trim();
+		let query = e.target.value.trim();
+		this.setMessage(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)
 				});
-				if (this.container.open) {
-					window.removeChildren(this.ui.terms.list);
-				}
 			},
 			100
 		);
 	}
 
+	setField(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) {
+			console.error('No field found...');
+			return;
+		}
+		this.activeField = fieldId;
+		this.setMessage(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) {
 		const fieldId = this.getFieldId(e.target);
 		const field = this.fields.get(fieldId);
@@ -272,8 +323,7 @@
 		window.debouncer.cancel(`${fieldId}-search-results`);
 
 		if (!this.container.open){
-			this.activeField = fieldId;
-			this.preloadTaxonomy(field.taxonomy);
+			this.setField(fieldId);
 		}
 	}
 
@@ -282,7 +332,8 @@
 		const fieldId = this.getFieldId(e.target);
 		const field = this.fields.get(fieldId);
 		if (!fieldId || ! field) return;
-		if (!field.hasAutocomplete) return;
+		if (!field.hasAutocomplete || this.container.open) return;
+		if (e.relatedTarget && field.ui.dropdown.wrapper?.contains(e.relatedTarget)) return;
 
 		this.scheduleHideDropdown(fieldId);
 	}
@@ -294,8 +345,12 @@
 		window.debouncer.schedule(
 			`${fieldId}-search-results`,
 			() => {
-				this.activeField = null;
-				field.ui.dropdown.hidden = true;
+				if (!this.container.open) {
+					this.activeField = null;
+				}
+				if (field.ui.dropdown.wrapper) {
+					field.ui.dropdown.wrapper.hidden = true;
+				}
 			},
 			1500
 		);
@@ -312,13 +367,14 @@
 			this.container,
 			{
 				handleForm: false,
-				save: null,
 				open: null
 			}
 		);
 		this.modal.subscribe((event, data) => {
 			switch (event) {
-
+				case 'modal-close':
+					this.closeModal()
+					break;
 			}
 		});
 	}
@@ -338,42 +394,24 @@
 		const field = this.fields.get(fieldId);
 		if (!field) return;
 
-		this.activeField = fieldId;
-		this.ui.modal.title.textContent = `Select ${field.plural}`;
+		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.ui.create.details) {
-			this.ui.create.details.hidden = !field.canCreate;
-
-			if (this.ui.create.summary) {
-				this.ui.create.summary.textContent = `Add new ${field.singular}`;
-			}
-			if (this.ui.create.label.name) {
-				this.ui.create.label.name.textContent = `Name this ${field.singular}`;
-			}
-			if (this.ui.create.label.parent) {
-				this.ui.create.label.parent.textContent = `Nest it under`;
-			}
+		if (this.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.setLoading();
-
-		this.store.setFilters({
-			taxonomy: field.taxonomy,
-			page: 1,
-			search: '',
-			parent: 0,
-		});
 
 		this.a11y.announce(message);
 	}
 
 	closeModal() {
-		this.modal.handleClose();
 		const field = this.fields.get(this.activeField);
 		if (!field) return;
 		this.observer.unobserve(this.ui.terms.sentinel);
@@ -394,7 +432,10 @@
 		const current = this.store.filters.parent;
 		if (current === 0) return;
 		let term = this.store.get(parseInt(current));
-		if (!term) return;
+		if (!term) {
+			this.navigateTo(0);
+			return;
+		}
 		let parent = term.parent;
 		this.navigateTo(parseInt(parent));
 	}
@@ -419,11 +460,17 @@
 	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;
 
 		const item = window.getTemplate('selectedTerm');
+		if (!item) return;
+
 		item.dataset.id = termId;
-		item.querySelector('span').textContent = term.path;
-		item.querySelector('button').title = `Remove ${name}`;
+		item.dataset.taxonomy = field.taxonomy;
+		item.querySelector('.item-name').textContent = term.path;
+		item.querySelector('button').title = `Remove ${term.name}`;
 
 		this.ui.selected.append(item);
 	}
@@ -448,7 +495,7 @@
 
 	registerField(element, options = {}) {
 		let input = element.querySelector('input[type="hidden"]');
-		if (!input) {
+		if (!input && !Object.hasOwn(element.dataset, 'filter')) {
 			console.warn('TaxonomySelector: No hidden input found for field', element);
 			return;
 		}
@@ -460,11 +507,20 @@
 
 
 		let selectors = this.selectors.field;
-		let button = element.querySelector('button.taxonomy-toggle');
-		if (options.size === 0){
+		const isFilter = Object.hasOwn(element.dataset,'filter') && element.dataset.filter === 'taxonomy';
+		let button = (isFilter) ? element : element.querySelector('button.taxonomy-toggle');
+
+		if (Object.keys(options).length === 0){
 			if (!button) return;
-			options = button.dataset;
-			if (options.size === 0) 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')
+			};
+			if (Object.keys(options).length === 0) return;
 		} else if (Object.hasOwn(options, 'toggle')) {
 			button = document.querySelector(options.toggle);
 			selectors.toggle = options.toggle;
@@ -478,21 +534,38 @@
 			singular: options.single??'',
 			plural: options.plural??'',
 			name: element.dataset.field,
-			canSearch: Object.hasOwn(options, 'search'),
+			canSearch: options.search??false,
 			limit: options.limit??0,
-			hasAutocomplete: Object.hasOwn(options, 'autocomplete'),
-			canCreate: Object.hasOwn(options, 'creatable'),
-			isRequired: Object.hasOwn(options, 'required'),
+			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 (!config.taxonomy) return;
+
+		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(input);
+		this.setSelectedFromValue(fieldId, input);
 
 
 		if (this.isInitializing) {
@@ -504,12 +577,19 @@
 	}
 
 	setSelectedFromValue(fieldId, input) {
+		if (!fieldId) return;
+		let field = this.fields.get(fieldId);
+		if (!field) return;
+		if (!input && !field.isFilter) return;
+
 		let selected = new Set();
-		input.value.value.trim()
-			.split(',')
-			.map(id => parseInt(id.trim()))
-			.filter(id => !isNaN(id))
-			.forEach(id => selected.add(id));
+		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);
 	}
 
@@ -524,8 +604,10 @@
 		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.updateFieldValue(fieldId);
 		this.checkLimits(fieldId);
 	}
 	removeSelected(termId, fieldId = null) {
@@ -535,34 +617,42 @@
 		if (!field || !term) return;
 		this.selectedTerms.get(fieldId).delete(parseInt(termId));
 
-		const selectedItem = field.ui.selected.querySelector(`[data-i"${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.querySelector(`[data-id="${termId}"]`);
+			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;
+			}
 		}
-		this.updateFieldValue(fieldId);
+		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));
-		field.ui.value = selected.join(',');
+		field.ui.value.value = selected.join(',');
 	}
 
 	checkLimits(fieldId) {
 		if (!this.container.open) return;
 		const field = this.fields.get(fieldId);
-		if (!field || field.limit === 0) return;
+		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 (!fieldId || !field) return;
+		if(!field) return;
 
 		this.setSelectedFromValue(fieldId, input);
 		this.updateFieldUI(fieldId);
@@ -570,8 +660,8 @@
 
 	updateFieldUI(fieldId) {
 		const field = this.fields.get(fieldId);
-		let selected = this.selectedTerms.get(fieldId);
-		if (!field || selected.size === 0) return;
+		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);
@@ -585,8 +675,8 @@
 			.some(term=>term.taxonomy === taxonomy);
 
 		fields.forEach(field => {
-			field.ui.toggle.disabled = !hasItems && !field.canCreate;
-			field.ui.toggle.title = !hasItems
+			field.toggle.disabled = !hasItems && !field.canCreate;
+			field.toggle.title = !hasItems
 				? `No ${field.singular} available`
 				: `Select ${field.plural}`;
 
@@ -594,11 +684,30 @@
 		});
 	}
 
-	showModalTerms(append = true, showPath = false) {
+	showModalTerms(showPath = false) {
+		const field = this.currentField();
 		const terms = this.store.getFiltered();
-		if (terms.size === 0) return;
-		if (!append) {
-			window.removeChildren(this.ui.terms.list);
+		if (terms.length === 0) {
+			if (this.store.filters.page??1 === 1) {
+				window.removeChildren(this.ui.terms.list);
+			}
+			this.setMessage(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);
+			}
+		}
+
+		this.setCreateButton(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;
@@ -611,10 +720,14 @@
 				... term
 			});
 			if (element) {
-				fragment.appendChild(element);
+				fragment.append(element);
 			}
 		});
 
+		if (terms.length > 0) {
+			this.setMessage(false);
+		}
+
 		this.ui.terms.list.append(fragment);
 	}
 	createTermElement(term) {
@@ -638,6 +751,7 @@
 		let limitReached = field.limit > 0 && this.selectedTerms.get(this.activeField).size >= field.limit;
 		if (checkbox && label && nameSpan) {
 			[
+				checkbox.dataset.id,
 				checkbox.id,
 				checkbox.name,
 				checkbox.value,
@@ -648,8 +762,9 @@
 				label.dataset.path,
 				nameSpan.textContent
 			] = [
+				term.id,
 				`${field.element.id}-${term.id}`,
-				`${field.container.id}-${field.taxonomy}-select`,
+				`${field.element.id}-${field.taxonomy}-select`,
 				term.id,
 				!isSelected && limitReached,
 				isSelected,
@@ -673,30 +788,28 @@
 	showAutocompleteTerms() {
 		const field = this.currentField();
 		const terms = this.currentTerms();
-		if (!field || terms.size ===0) return;
+		if (!field) return;
 
-		const dropdown = field.ui.dropdown;
+		const dropdown = field.ui.dropdown.list;
+		if (!dropdown) return;
+
 		window.removeChildren(dropdown);
 		if (terms.length === 0) {
-			this.showEmptyState(`No ${field.plural} found.`, dropdown);
+			this.setMessage(true, `No ${field.plural} found.`, false);
 		} else {
 			terms.forEach(term => {
 				const item = this.createAutocompleteTerm(term);
 				if (item) {
 					dropdown.append(item);
 				}
-			})
+			});
+			this.setMessage(false);
 		}
+		this.setCreateButton(true);
 
-		const query = field.ui.search?.value;
-		if (field.canCreate && query.length >= 2 && this.creator) {
-			const createButton = this.createTermButton(query);
-			if (createButton) {
-				dropdown.append(createButton);
-			}
+		if (field.ui.dropdown.wrapper) {
+			field.ui.dropdown.wrapper.hidden = false;
 		}
-
-		dropdown.hidden = false;
 	}
 	createAutocompleteTerm(term) {
 		const item = window.getTemplate('autocompleteItem');
@@ -704,6 +817,7 @@
 
 		item.dataset.id = term.id;
 		item.textContent = term.path || term.name;
+
 		return item;
 	}
 	/******************************************************************
@@ -714,7 +828,7 @@
 		const field = this.fields.get(fieldId);
 		if (!term || !field) return;
 		//if the term already exists in the selected items, bail early
-		if (field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
+		if (field.ui.selected && field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
 
 		const item = window.getTemplate('selectedTerm');
 		if (!item) return;
@@ -724,7 +838,9 @@
 		item.querySelector('.item-name').textContent = term.path;
 		item.querySelector('button').title = `Remove ${term.name}`;
 
-		field.ui.selected.append(item);
+		if (field.ui.selected) {
+			field.ui.selected.append(item);
+		}
 
 		if (this.container.open) {
 			this.addTermToModal(termId);
@@ -732,15 +848,6 @@
 			if (checkbox) checkbox.checked = true;
 		}
 	}
-	createTermButton(query) {
-		const button = window.getTemplate('autocompleteButton');
-		if(!button) return;
-
-		let queryEl = button.querySelector('span');
-		queryEl.textContent = `"${query}"`;
-
-		return button;
-	}
 
 	updateBreadcrumbs(termId) {
 		const nav = this.ui.nav.nav;
@@ -826,15 +933,22 @@
 	handleStoreEvent(event, data) {
 		const handlers = {
 			'data-loaded': () => this.handleDataLoaded(),
-			'filters-changed': () => this.handleFiltersChanged(),
+			'filters-changed': () => this.handleFiltersChanged(data),
 			'fetch-error': () => this.handleFetchError()
 		};
 
-		handlers[event]?.();
+		try {
+			handlers[event]?.(data);
+		} catch (error) {
+			console.error(`Error handling store event "${event}":`, error);
+			this.setMessage(true, 'An error occurred loading data', false);
+		}
 	}
 	handleDataLoaded() {
 		const taxonomy = this.store.filters.taxonomy;
-		if (taxonomy?.includes(',')) {
+
+		// Always update fields for loaded taxonomies (handles both single and batch)
+		if (taxonomy) {
 			const taxonomies = taxonomy.split(',').map(t => t.trim());
 			taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
 		}
@@ -845,63 +959,52 @@
 		}
 		if (this.activeField) {
 			this.showResults(true);
+			return;
 		}
+		this.setMessage(false);
 	}
 
 	showResults(isAutoComplete = false) {
-		this.setLoading(false);
+		this.setMessage(false);
 		const terms = this.store.getFiltered();
 		const filters = this.store.filters;
-		const response = this.store.lastResponse?.page || {};
 		const isSearch = filters.search && filters.search.length > 0;
-		const append = filters.page > 1;
-		const field = this.currentField();
 
 		this.notify('terms-loaded', {
 			terms,
 			filters
 		});
 
-		if (terms.length === 0) {
-			if (!append) {
-				this.showEmptyState(isSearch ? `No matching ${field.plural}.` : `No ${field.plural} available.`);
-			}
-			this.observer.unobserve(this.ui.terms.sentinel);
-		} else {
-			if (!isAutoComplete) {
-				this.showModalTerms(append, isSearch);
 
-				if (response.has_more) {
-					this.observer.observe(this.ui.terms.sentinel);
-				} else {
-					this.observer.unobserve(this.ui.terms.sentinel);
-				}
-			} else {
-				this.showAutocompleteTerms()
-			}
+		if (isAutoComplete) {
+			this.showAutocompleteTerms();
+		} else {
+			this.showModalTerms(isSearch);
 		}
 
-		this.a11y.announce(terms.length, append);
+
+		this.a11y.announce(terms.length);
 	}
-	handleFiltersChanged() {
-		// if (this.modal?.open) {
-		// 	this.setLoading();
-		// }
+	handleFiltersChanged(data) {
+		//maybe do something?
 	}
 
 	handleFetchError(error) {
-		this.setLoading(false);
+		const field = this.currentField();
+		const message = field
+			? `Failed to load ${field.plural}`
+			: 'Failed to load data';
+
+		this.setMessage(true, message, false);
+		console.error('Store fetch error:', error);
 	}
 	async batchFetchTaxonomies() {
 		if (this.batchFetch.size === 0) return;
 
 		const taxonomies = Array.from(this.batchFetch);
-		taxonomies.forEach(tax => this.loadedTaxonomies.add(tax));
 		this.batchFetch.clear();
 
 		try {
-			taxonomies.forEach(tax => this.loadedTaxonomies.add(tax));
-
 			await this.store.setFilters({
 				taxonomy: taxonomies.join(','),
 				page: 1,
@@ -914,55 +1017,112 @@
 	}
 
 	preloadTaxonomy(taxonomy) {
-		if (this.loadedTaxonomies.has(taxonomy)) return;
-
 		this.store.setFilters( {
 			taxonomy: taxonomy,
 			page: 1,
 			search: '',
 			parent: 0
 		});
-
-		this.loadedTaxonomies.add(taxonomy);
 	}
 
 	/**************************************************
 	 LOADING
 	**************************************************/
-	setLoading(on = true) {
-		this.ui.loading.loading.hidden = on;
-		this.modal.classList.toggle('loading', on);
+	setCreateButton(show = true) {
+		const field = this.currentField();
+		if (!field || !field.canCreate || !this.creator) return;
 
-		if (on) {
-			let searchQuery = this.store.filters.search || '';
-			searchQuery = searchQuery === '' ? false : searchQuery;
-			const currentParent = this.store.filters.parent || 0;
-			const message = searchQuery
-				? `Searching for "${searchQuery} items` :
-				currentParent === 0
-					? 'loading items'
-					: 'loading child items';
+		const conf = (this.container.open) ? this.ui : field.ui;
+		if (!conf.create?.button || !conf.create?.span) return;
 
-			if (window.typeLoop && this.ui.loading.text) {
-				this.stopTyping = window.typeLoop(this.ui.loading.text, message);
-			} else {
-				this.ui.loading.text.textContenet = message;
-			}
-		} else {
-			if (this.stopTyping) {
-				this.stopTyping();
-				this.stopTyping = null;
-			}
+		const createButton = conf.create.button;
+		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??'';
 		}
 	}
-	showEmptyState(message = 'No items found.', container = null) {
-		if (!container) container = this.ui.terms.list;
-		const emptyElement = window.getTemplate('noTermResults');
-		const span = emptyElement.querySelector('span');
-		if (message && span) {
-			span.textContent = message;
+	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
 		}
-		container.append(emptyElement);
+		//If it's autocomplete or the selector's search input, we just need the name
+		if (!this.container.open || this.ui.search.input.value !== '') {
+			data.name = (this.container.open) ? this.ui.search.input.value : field.ui.search.value;
+		} else {
+			//Otherwise, we've created it from the details element
+			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(true, `Creating "${data.name}"...`);
+			this.setCreateButton(false);
+			if (this.container.open) {
+				window.removeChildren(this.ui.terms.list);
+			} else {
+				field.ui.search.disabled = true;
+				window.removeChildren(field.ui.dropdown.list);
+				if (field.ui.dropdown.wrapper) {
+					field.ui.dropdown.wrapper.hidden = false;
+				}
+			}
+			let term = await this.creator.handleTermCreation(data);
+			if (term) {
+				this.addSelected(term.id, field.id);
+			}
+			if (!this.container.open) {
+				field.ui.search.disabled = false;
+				field.ui.search.value = '';
+			}
+			this.scheduleHideDropdown(field.id);
+			this.setMessage(false);
+		}
+	}
+	setMessage(show = true, message = '', type = true) {
+		const field = this.currentField();
+		if (!field) return;
+
+		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
@@ -984,16 +1144,49 @@
 	 CLEANUP
 	******************************************************/
 	destroy() {
+		// 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();
+
+		// Remove event listeners
 		document.removeEventListener('click', this.clickHandler);
 		document.removeEventListener('change', this.changeHandler);
 		document.removeEventListener('input', this.inputHandler);
-		document.removeEventListener('focus', this.focusHandler);
-		document.removeEventListener('blur', this.blurHandler);
+		document.removeEventListener('focus', this.focusHandler, true);
+		document.removeEventListener('blur', this.blurHandler, true);
 
-		this.observer?.disconnect();
+		// Clear data structures
 		this.subscribers.clear();
 		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;
+		}
 	}
 }
 

--
Gitblit v1.10.0