From d7e7d248cbe41cd7a9ef9c2fb022b6c4831f99a3 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 31 May 2026 15:22:56 +0000
Subject: [PATCH] =jakevan complete
---
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