Jake Vanderwerf
8 days ago 3b83905603d44b1a08f8b2b36a605808ce686ad6
assets/js/concise/TaxonomyCreator.js
@@ -1,233 +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));
            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 = '';
         }
      } 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);
         }
@@ -242,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) {
@@ -301,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
@@ -376,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;
      }
   }
}