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

---
 assets/js/concise/TaxonomyCreator.js |  404 +++++++++++++++++++--------------------------------------
 1 files changed, 133 insertions(+), 271 deletions(-)

diff --git a/assets/js/concise/TaxonomyCreator.js b/assets/js/concise/TaxonomyCreator.js
index 7307202..b9adaf7 100644
--- a/assets/js/concise/TaxonomyCreator.js
+++ b/assets/js/concise/TaxonomyCreator.js
@@ -1,233 +1,185 @@
 /**
- * This separates out all create logic from the base TaxonomySelector.js
- * Updated to work with centralized DataStore architecture
+ * TaxonomyCreator - Handles term creation for TaxonomySelector
  */
-
 class TaxonomyCreator {
 
 	constructor(selector) {
 		this.selector = selector;
+		this.queue = window.jvbQueue;
 
-		// Only initialize modal elements if modal exists
-		if (selector.modal) {
-			this.createNew = selector.modal.querySelector('.create-new-term');
-			this.toggle = selector.modal.querySelector('.new-term-toggle');
-			this.form = this.createNew?.querySelector('.create-new-term-section');
-		}
-
+		this.initElements();
 		this.initListeners();
-
-		// Only init term creation UI if we have modal elements
-		if (this.form) {
-			this.initTermCreation();
-		}
 	}
 
+	initElements() {
+		this.selectors = {
+			details: 'details.create-new',
+			parent: '#select_parent',
+			summary: '.create-new summary',
+			suggestion: '.term-suggestions',
+			name: '#term_name',
+			button: '.submit-term',
+			form: 'form.create-term',
+			label: {
+				name: '[for="term_name"]',
+				parent: '[for="select_parent"]'
+			},
+			loading: '.loading-message.create-term'
+		};
+		this.ui = window.uiFromSelectors(this.selectors, this.selector.container);
+	}
+
+	/**
+	 * Initialize event listeners
+	 */
 	initListeners() {
 		this.clickHandler = this.handleClick.bind(this);
 		document.addEventListener('click', this.clickHandler);
+
+		if (this.ui.form) {
+			this.ui.form.addEventListener('change', (e) => {
+				e.preventDefault();
+				e.stopPropagation();
+			})
+		}
 	}
 
+	/**
+	 * Handle click events
+	 */
 	handleClick(e) {
-		if (window.targetCheck(e, '.create-new-term summary')) {
-			if (this.createNew.open) {
-				this.createNew.querySelector('input[name="term_name"]').focus();
+		// Handle opening create term form
+		if (window.targetCheck(e, this.selectors.summary)) {
+			if (this.ui.details.open) {
+				this.ui.name?.focus();
 			}
 			this.resetParentOptions();
 		}
 
-		if (window.targetCheck(e, '.submit-term')) {
-			this.handleTermCreation(e).then(()=>{});
-		}
-
-		// Handle autocomplete create button
-		if (window.targetCheck(e, '.create-term')) {
-			this.handleAutocompleteCreate(e).then(()=>{});
+		// Handle term creation submission
+		if (window.targetCheck(e, this.selectors.button)) {
+			this.handleTermCreation(e).then(() => {});
 		}
 	}
 
+	/**
+	 * Handle term creation from modal form
+	 */
 	async handleTermCreation(e) {
-		const taxonomy = this.selector.currentConfig?.taxonomy;
+		let field = this.selector.currentField();
+		const taxonomy = field.taxonomy;
 		if (!taxonomy) return;
 
-		const termName = this.form.querySelector('input[name="term_name"]').value.trim();
-		const parentId = parseInt(this.form.querySelector('input#select_parent')?.value) || 0;
-
-		if (!termName) return;
+		let data = {
+			parent: 0,
+			taxonomy: taxonomy
+		};
+		// If it's open, we can get the parent element and everything
+		if (this.selector.container.open) {
+			data.name = this.ui.name.value.trim();
+			data.parent = parseInt(this.ui.parent?.value??0);
+		} else if (this.selector.activeField) {
+			//It's autocomplete, so just the term name
+			data.name = this.selector.store.query;
+		}
+		if (!data.name || data.name.length < 2) return;
 
 		try {
-			const submitButton = this.form.querySelector('button');
-			if (submitButton) {
-				submitButton.disabled = true;
+			if (this.ui.button) {
+				this.ui.button.disabled = true;
 			}
-			const response = await this.createTerm(termName, parentId, taxonomy);
+
+			const response = await this.createTerm(data);
 
 			if (response.success && response.term) {
-				let term = response.term;
-				const termPath = term.path || term.name;
-
-				this.createNew.open = false;
-				await this.selector.store.clearCache();
-
-				this.selector.store.data.set(term.id, {
-					id: term.id,
-					name: term.name,
-					path: termPath,
-					taxonomy: taxonomy,
-					parent: parentId,
-					count: 0,
-					hasChildren: false,
-					slug: term.slug || termName.toLowerCase().replace(/\s+/g, '-')
-				});
-
-				this.selector.addSelectedTermToModal(term.id, term.name, termPath);
-
-				const currentParent = this.selector.store.filters.parent || 0;
-				if (currentParent === parentId) {
-					await this.selector.store.setFilters({
-						taxonomy,
-						parent: parentId,
-						page: 1,
-						search: ''
-					});
-				}
-
-				this.form.querySelector('input[name="term_name"]').value = '';
-				const suggestionContainer = this.createNew.querySelector('.term-suggestions');
-				if (suggestionContainer) {
-					suggestionContainer.hidden = true;
-				}
-
+				await this.handleSuccessfulCreation(response.term, data);
+				this.clearForm();
 			}
 		} catch (error) {
 			console.error('Error creating term:', error);
-			this.selector.error?.log(error, {
-				component: 'TaxonomyCreator',
-				action: 'handleTermCreation'
-			});
+			this.selector.handleError(error, 'handleTermCreation');
 		} finally {
-			this.form.querySelector('button').disabled = false;
-		}
-	}
-
-	async handleAutocompleteCreate(e) {
-		const button = e.target.closest('.create-term');
-		const fieldId = this.selector.getFieldId(button);
-		const field = this.selector.fields.get(fieldId);
-
-		if (!field) return;
-
-		const input = field.container.querySelector('input[data-autocomplete]');
-		const termName = input?.value.trim() || button.dataset.query;
-
-		if (!termName) return;
-
-		const originalHTML = button.innerHTML;
-
-		try {
-			button.disabled = true;
-			button.textContent = 'Creating...';
-
-			const response = await this.createTerm(termName, 0, field.taxonomy);
-
-			if (response.success && response.term) {
-				const term = response.term;
-				const termPath = term.path || term.name;
-
-				field.selectedTerms.add(parseInt(term.id));
-				this.selector.store.data.set(term.id, {
-					id: term.id,
-					name: term.name,
-					path: termPath,
-					taxonomy: field.taxonomy,
-					parent: 0,
-					count: 0,
-					hasChildren: false,
-					slug: term.slug || termName.toLowerCase().replace(/\s+/g, '-')
-				});
-				this.selector.addTermToDisplay(field.id, term.id, term.name, termPath);
-
-				field.input.value = Array.from(field.selectedTerms).join(',');
-				field.input.dispatchEvent(new Event('change', { bubbles: true }));
-
-				field.autocompleteDropdown.hidden = true;
-				if (input) input.value = '';
-
-				// Clear cache AND refresh this taxonomy's data
-				this.selector.store.clearCache();
-
-				// Trigger a background refresh for this taxonomy
-				await this.selector.store.setFilters({
-					taxonomy: field.taxonomy,
-					page: 1,
-					search: '',
-					parent: 0
-				});
-			} else if (response.reason === 'exists' && response.term) {
-				const term = response.term;
-				field.selectedTerms.add(parseInt(term.id));
-				this.selector.addTermToDisplay(field.id, term.id, term.name, term.path || term.name);
-
-				field.input.value = Array.from(field.selectedTerms).join(',');
-				field.input.dispatchEvent(new Event('change', { bubbles: true }));
-
-				field.autocompleteDropdown.hidden = true;
-				if (input) input.value = '';
+			if (this.ui.button) {
+				this.ui.button.disabled = false;
 			}
-		} catch (error) {
-			console.error('Error creating term:', error);
-			this.selector.error?.log(error, {
-				component: 'TaxonomyCreator',
-				action: 'handleAutocompleteCreate'
-			});
-		} finally {
-			button.innerHTML = originalHTML;
-			button.disabled = false;
 		}
 	}
 
-	initTermCreation() {
-		if (!this.form) {
-			return;
-		}
+	/**
+	 * Handle successful term creation
+	 */
+	async handleSuccessfulCreation(term, data) {
+		const termPath = term.path || term.name;
+		// Close create form
+		this.ui.details.open = false;
 
-		this.form.addEventListener('change', (e) => {
-			e.preventDefault();
-			e.stopPropagation();
+		// Clear cache to ensure fresh data
+		await this.selector.store.clearCache();
+
+		// Add to DataStore
+		this.selector.store.data.set(term.id, {
+			id: term.id,
+			name: term.name,
+			path: termPath,
+			taxonomy: data.taxonomy,
+			parent: data.parent,
+			count: 0,
+			hasChildren: false,
+			slug: term.slug || term.name.toLowerCase().replace(/\s+/g, '-')
 		});
+
+		// Add to modal selection
+		this.selector.addSelected(term.id,this.selector.activeField);
+
+		// Refresh current view if we're viewing the same parent
+		if (this.selector.container.open) {
+			const currentParent = this.selector.store.filters.parent || 0;
+			if (currentParent === data.parent) {
+				await this.selector.store.setFilters({
+					taxonomy: data.taxonomy,
+					parent: data.parent,
+					page: 1,
+					search: ''
+				});
+			}
+		}
 	}
 
+	/**
+	 * Reset parent options in create form
+	 */
 	resetParentOptions() {
-		const taxonomy = this.selector.currentConfig?.taxonomy;
+		const field = this.selector.currentField();
+		if (!field) return;
+		const taxonomy = field.taxonomy;
 		if (!taxonomy) return;
 
-		let select = this.createNew.querySelector('#select_parent');
-		if (!select) return;
+		if (!this.ui.parent) return;
 
-		let defaultOption = select.querySelector('option');
+		let defaultOption = this.ui.parent.querySelector('option');
 		if (!defaultOption) return;
 
 		// Clear existing options
-		window.removeChildren(select);
-		select.append(defaultOption.cloneNode(true));
+		window.removeChildren(this.ui.parent);
+		this.ui.parent.append(defaultOption.cloneNode(true));
 
 		// Get current parent from store filters
 		const currentParent = this.selector.store.filters.parent || 0;
 
 		// If we're in a sub-category, add the current parent as an option
 		if (currentParent !== 0) {
-			const parentTerm = this.selector.store.data.get(currentParent);
+			const parentTerm = this.selector.store.get(currentParent);
 			if (parentTerm) {
 				let parentOption = defaultOption.cloneNode(true);
 				parentOption.value = parentTerm.id;
 				parentOption.textContent = parentTerm.name;
-				select.append(parentOption);
+				this.ui.parent.append(parentOption);
 			}
 		}
 
-		// Add all terms currently visible in the taxonomy (from store cache)
+		// Add all terms currently visible in the taxonomy
 		const visibleTerms = [];
-		this.selector.store.data.forEach(term => {
+		this.selector.store.getFiltered().forEach(term => {
 			if (term.taxonomy === taxonomy && term.parent === currentParent) {
 				visibleTerms.push(term);
 			}
@@ -242,50 +194,24 @@
 			option.id = `select-parent-${term.id}`;
 			option.value = term.id;
 			option.textContent = '  — ' + term.name;
-			select.append(option);
+			this.ui.parent.append(option);
 		});
 	}
 
-	async createTerm(name, parent = 0, taxonomy) {
+	/**
+	 * Create a new term
+	 */
+	async createTerm(data) {
+		if (!data.name || data.parent === undefined || !data.taxonomy) return;
 		try {
-			// Search to ensure we have latest data for duplicate check
-			await this.selector.store.setFilters({
-				taxonomy: taxonomy,
-				search: name,
-				page: 1,
-				parent: 0
-			});
 
-			// Wait a bit for the data to load
-			await new Promise(resolve => setTimeout(resolve, 100));
-
-			// Check if exact match exists in results
-			const exactMatch = Array.from(this.selector.store.data.values())
-				.find(term =>
-					term.taxonomy === taxonomy &&
-					term.name.toLowerCase() === name.toLowerCase()
-				);
-
-			if (exactMatch) {
-				// For modal context, show suggestions
-				if (this.createNew) {
-					this.showTermSuggestions([exactMatch], true);
-				}
-				return { success: false, reason: 'exists', term: exactMatch };
-			}
-
-			// Term doesn't exist, create it
 			const response = await fetch(`${jvbSettings.api}terms`, {
 				method: 'POST',
 				headers: {
 					'Content-Type': 'application/json',
 					'X-WP-Nonce': window.auth.getNonce()
 				},
-				body: JSON.stringify({
-					taxonomy: taxonomy,
-					name: name,
-					parent: parent
-				})
+				body: JSON.stringify(data)
 			});
 
 			if (!response.ok) {
@@ -301,73 +227,16 @@
 	}
 
 	/**
-	 * Show term suggestions when similar terms exist
+	 * Clear the creation form
 	 */
-	showTermSuggestions(suggestions, isExact = false) {
-		const suggestionContainer = this.createNew.querySelector('.term-suggestions') ||
-			this.createSuggestionContainer();
-
-		// Clear existing suggestions
-		window.removeChildren(suggestionContainer);
-
-		// Add heading
-		const heading = document.createElement('h4');
-		heading.textContent = isExact ?
-			'This term already exists:' :
-			'Similar terms already exist:';
-		suggestionContainer.appendChild(heading);
-
-		// Create list of suggestions
-		const list = document.createElement('ul');
-		list.className = 'term-suggestion-list';
-
-		suggestions.forEach(term => {
-			const item = document.createElement('li');
-
-			const button = document.createElement('button');
-			button.type = 'button';
-			button.className = 'use-existing-term';
-			button.setAttribute('data-id', term.id);
-			button.textContent = term.path || term.name;
-
-			button.addEventListener('click', () => {
-				// Add this term to modal selection
-				this.selector.addSelectedTermToModal(term.id, term.name, term.path || term.name);
-
-				// Close the create new section
-				this.createNew.open = false;
-
-				// Clear suggestions
-				suggestionContainer.hidden = true;
-
-				// Clear the form
-				this.form.querySelector('input[name="term_name"]').value = '';
-			});
-
-			item.appendChild(button);
-			list.appendChild(item);
-		});
-
-		suggestionContainer.appendChild(list);
-		suggestionContainer.hidden = false;
+	clearForm() {
+		if (this.ui.name) {
+			this.ui.name.value = '';
+		}
 	}
 
 	/**
-	 * Create container for term suggestions if it doesn't exist
-	 */
-	createSuggestionContainer() {
-		const container = document.createElement('div');
-		container.className = 'term-suggestions';
-		container.hidden = true;
-
-		// Insert after the form
-		this.createNew.querySelector('form').after(container);
-		return container;
-	}
-
-
-	/**
-	 * Clean up when modal closes
+	 * Clean up when destroyed
 	 */
 	destroy() {
 		// Remove event listeners
@@ -376,15 +245,8 @@
 		}
 
 		// Clear any pending operations
-		const loadingMessage = this.createNew?.querySelector('.loading-message.create-term');
-		if (loadingMessage) {
-			loadingMessage.hidden = true;
-		}
-
-		// Clear suggestions
-		const suggestionContainer = this.createNew?.querySelector('.term-suggestions');
-		if (suggestionContainer) {
-			suggestionContainer.hidden = true;
+		if (this.ui.loading) {
+			this.ui.loading.hidden = true;
 		}
 	}
 }

--
Gitblit v1.10.0