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

---
 assets/js/concise/TaxonomyCreator.js |  441 +++++++++++++++++++-----------------------------------
 1 files changed, 156 insertions(+), 285 deletions(-)

diff --git a/assets/js/concise/TaxonomyCreator.js b/assets/js/concise/TaxonomyCreator.js
index 3c5f24d..922c3a4 100644
--- a/assets/js/concise/TaxonomyCreator.js
+++ b/assets/js/concise/TaxonomyCreator.js
@@ -1,230 +1,188 @@
 /**
- * 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-term',
+			parent: '#select_parent',
+			summary: '.create-term 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);
+	}
+
+	handleOpen(field) {
+		this.field = field;
+		if (this.ui.details) {
+			this.ui.details.hidden = !field.canCreate;
+
+			if (this.ui.summary) {
+				this.ui.summary.textContent = `Add new ${field.singular}`;
+			}
+			if (this.ui.label.name) {
+				this.ui.label.name.textContent = `Name this ${field.singular}`;
+			}
+			if (this.ui.label.parent) {
+				this.ui.label.parent.textContent = `Nest it under`;
+			}
 		}
 	}
 
+	/**
+	 * 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(()=>{});
-		}
-	}
-
-	async handleTermCreation(e) {
-		const taxonomy = this.selector.currentConfig?.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;
-
-		try {
-			const submitButton = this.form.querySelector('button');
-			if (submitButton) {
-				submitButton.disabled = true;
-			}
-			const response = await this.createTerm(termName, parentId, taxonomy);
-
-			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;
-				}
-
-			}
-		} catch (error) {
-			console.error('Error creating term:', error);
-			this.selector.error?.log(error, {
-				component: 'TaxonomyCreator',
-				action: '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));
-
-				// Add to store's data map
-				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 ALL cache for this taxonomy
-				// This forces next search to hit the server
-				await this.selector.store.clearCache();
-
-			} 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 = '';
-			}
-		} 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;
 		}
+	}
 
-		this.form.addEventListener('change', (e) => {
-			e.preventDefault();
-			e.stopPropagation();
+	/**
+	 * Handle term creation from modal form
+	 */
+	async handleTermCreation(data) {
+		if (!data.name || data.name.length < 2) return false;
+		try {
+			const response = await this.createTerm(data);
+			let currentField = this.selector.currentField();
+			if (!response.success) {
+				// Term already exists - still add it
+				if (response.term && response.term.id) {
+					this.selector.setMessage(currentField,true, `Using existing "${response.term.name}"`);
+					return response.term;
+				}
+
+				// Other failure
+				this.selector.setMessage(currentField,true, response.message || 'Creation failed', false);
+				return false;
+			}
+			if (response.term?.pending) {
+				// Term requires approval
+				this.selector.setMessage(
+					currentField,
+					true,
+					`"${data.name}" submitted for approval`,
+					false
+				);
+				// Don't add to selection since it's pending
+				return false;
+			}
+			if (response.success && response.term) {
+				await this.handleSuccessfulCreation(response.term, data);
+				this.clearForm();
+			}
+			return response.term;
+		} catch (error) {
+			console.error('Error creating term:', error);
+			return false;
+		}
+	}
+
+	/**
+	 * Handle successful term creation
+	 */
+	async handleSuccessfulCreation(term, data) {
+		// Add term to store immediately - don't wait for fetch
+		const fullTerm = {
+			id: term.id,
+			name: term.name,
+			path: term.path || term.name,
+			slug: term.slug || term.name.toLowerCase().replace(/\s+/g, '-'),
+			parent: data.parent || 0,
+			taxonomy: data.taxonomy,
+			count: 0,
+			hasChildren: false
+		};
+
+		// Add to store data immediately so addSelected can find it
+		this.selector.store.data.set(term.id, fullTerm);
+
+		// Close create form
+		if (this.ui.details) {
+			this.ui.details.open = false;
+		}
+
+		// Clear cache and refetch in background for accuracy
+		this.selector.store.clearCache();
+		// Don't await - let it happen async
+		this.selector.store.fetch().catch(err => {
+			console.warn('Background fetch after term creation failed:', err);
 		});
 	}
 
+	/**
+	 * 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);
 			}
@@ -239,50 +197,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) {
@@ -298,73 +230,19 @@
 	}
 
 	/**
-	 * 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 = '';
+		}
+		if (this.selector.ui.search.input){
+			this.selector.ui.search.input.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
@@ -373,15 +251,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