Jake Vanderwerf
2025-11-23 d7dbe7fee362d587dfc334135d9581b6216a4295
assets/js/dash/TaxonomyCreator.js
@@ -8,19 +8,19 @@
   constructor(selector) {
      this.selector = selector;
      // Get taxonomy from current active field config
      this.taxonomy = selector.currentConfig?.taxonomy;
      if (!this.taxonomy) {
         console.error('TaxonomyCreator: No active field or taxonomy found');
         return;
      // 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.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.initListeners();
      this.initTermCreation();
      // Only init term creation UI if we have modal elements
      if (this.form) {
         this.initTermCreation();
      }
   }
   initListeners() {
@@ -47,6 +47,9 @@
   }
   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;
@@ -54,98 +57,127 @@
      try {
         this.form.querySelector('button').disabled = true;
         const response = await this.createTerm(termName, parentId);
         const response = await this.createTerm(termName, parentId, taxonomy);
         if (response.success && response.term) {
            let term = response.term;
            // Close the create new section
            this.createNew.open = false;
            await this.selector.store.clearCache();
            // Invalidate the cache for this taxonomy
            await this.selector.store.invalidate({ taxonomy: this.taxonomy });
            // Add to store's data BEFORE display update
            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, '-')
            });
            // Add to current modal selection
            this.selector.addSelectedTermToModal(term.id, term.name, term.path || term.name);
            // If we're viewing the parent category where this was created, refresh the list
            const currentParent = this.selector.store.filters.parent || 0;
            if (currentParent === parentId) {
               await this.selector.store.setFilters({
                  taxonomy: this.taxonomy,
                  taxonomy,
                  parent: parentId,
                  page: 1,
                  search: ''
               });
            }
            // Clear the form
            this.form.querySelector('input[name="term_name"]').value = '';
            // Clear suggestions
            const suggestionContainer = this.createNew.querySelector('.term-suggestions');
            if (suggestionContainer) {
               suggestionContainer.hidden = true;
            }
            this.selector.store.cache.clear();
         }
      } catch (error) {
         console.error('Error creating term:', error);
         this.selector.error?.log(error, {
            component: 'TaxonomyCreator',
            action: 'handleTermCreation'
         }) || console.error('Failed to create term');
         });
      } finally {
         this.form.querySelector('button').disabled = false;
      }
   }
   async handleAutocompleteCreate(e) {
      const button = e.target;
      const button = e.target.closest('.create-term');
      const fieldId = this.selector.getFieldId(button);
      const field = this.selector.fields.get(fieldId);
      if (!field) return;
      // Get the current input value (not the debounced query)
      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;
         const originalText = button.innerHTML;
         button.textContent = 'Creating...';
         const response = await this.createTerm(termName, 0);
         const response = await this.createTerm(termName, 0, field.taxonomy);
         if (response.success && response.term) {
            const term = response.term;
            const termPath = term.path || term.name;
            // Add term to field
            field.selectedTerms.add(parseInt(term.id));
            this.selector.addTermToDisplay(field.id, term.id, term.name, term.path || term.name);
            // Add to store's data BEFORE display update
            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);
            // Update input
            field.input.value = Array.from(field.selectedTerms).join(',');
            field.input.dispatchEvent(new Event('change', { bubbles: true }));
            // Clear and hide dropdown
            field.autocompleteDropdown.hidden = true;
            if (input) input.value = '';
            // Refresh the store to include the new term
            await this.selector.store.setFilter('taxonomy', field.taxonomy);
         } else {
            button.innerHTML = originalText;
            this.selector.store.clearCache();
            await this.selector.store.setFilters({
               taxonomy: field.taxonomy,
               page: 1,
               search: '',
               parent: 0
            });
         } else if (response.reason === 'exists' && response.term) {
            // Term already exists - just add it
            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);
         button.textContent = 'Failed - Try again';
         setTimeout(() => {
            button.innerHTML = `<strong>Create:</strong> "${termName}"`;
            button.disabled = false;
         }, 2000);
         button.innerHTML = originalHTML;
         button.disabled = false;
         this.selector.error?.log(error, {
            component: 'TaxonomyCreator',
            action: 'handleAutocompleteCreate'
         });
      }
   }
@@ -161,6 +193,9 @@
   }
   resetParentOptions() {
      const taxonomy = this.selector.currentConfig?.taxonomy;
      if (!taxonomy) return;
      let select = this.createNew.querySelector('#select_parent');
      if (!select) return;
@@ -188,7 +223,7 @@
      // Add all terms currently visible in the taxonomy (from store cache)
      const visibleTerms = [];
      this.selector.store.data.forEach(term => {
         if (term.taxonomy === this.taxonomy && term.parent === currentParent) {
         if (term.taxonomy === taxonomy && term.parent === currentParent) {
            visibleTerms.push(term);
         }
      });
@@ -206,45 +241,32 @@
      });
   }
   async createTerm(name, parent = 0) {
      let loadingMessage = this.createNew.querySelector('.loading-message.create-term');
      let text = loadingMessage?.querySelector('span');
   async createTerm(name, parent = 0, taxonomy) {
      try {
         if (loadingMessage) {
            loadingMessage.hidden = false;
         }
         // Search for the exact term first
         await this.selector.store.setFilters({
            taxonomy: taxonomy,
            search: name,
            page: 1,
            parent: 0
         });
         if (text && window.typeText) {
            window.typeText(text, 'Checking term...');
         } else if (text) {
            text.textContent = 'Checking term...';
         }
         // 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()
            );
         // Search for existing terms with this name
         const searchResults = await this.searchExistingTerms(name);
         // Check for exact matches
         const exactMatches = searchResults.filter(term =>
            term.name.toLowerCase() === name.toLowerCase()
         );
         if (exactMatches.length > 0) {
            this.showTermSuggestions(exactMatches, true);
            return { success: false, reason: 'exists' };
         }
         // Show similar terms if found
         if (searchResults.length > 0) {
            this.showTermSuggestions(searchResults, false);
            return { success: false, reason: 'similar' };
         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
         if (text) {
            text.textContent = 'Creating term...';
         }
         const response = await fetch(`${jvbSettings.api}terms`, {
            method: 'POST',
            headers: {
@@ -252,7 +274,7 @@
               'X-WP-Nonce': jvbSettings.nonce
            },
            body: JSON.stringify({
               taxonomy: this.taxonomy,
               taxonomy: taxonomy,
               name: name,
               parent: parent
            })
@@ -267,18 +289,13 @@
      } catch (error) {
         console.error('Error creating term:', error);
         throw error;
      } finally {
         this.form.querySelector('button').disabled = false;
         if (loadingMessage) {
            loadingMessage.hidden = true;
         }
      }
   }
   /**
    * Search for existing terms using the store
    */
   async searchExistingTerms(searchQuery) {
   async searchExistingTerms(searchQuery, taxonomy) {
      return new Promise((resolve) => {
         // Set up a one-time listener for the search results
         const handleSearchResults = (event, data) => {
@@ -292,7 +309,7 @@
         // Trigger search
         this.selector.store.setFilters({
            taxonomy: this.taxonomy,
            taxonomy: taxonomy,
            search: searchQuery,
            page: 1,
            parent: 0
@@ -365,71 +382,6 @@
      return container;
   }
   /**
    * Create "Create new term" option for autocomplete dropdown
    */
   createAutocompleteOption(query, field) {
      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'autocomplete-item create-term';
      button.innerHTML = `<span>Create "${query}"</span>`;
      button.dataset.query = query;
      button.dataset.fieldId = field.id;
      button.addEventListener('click', async () => {
         await this.handleAutocompleteCreate(button, query, field);
      });
      return button;
   }
   /**
    * Handle term creation from autocomplete
    */
   async handleAutocompleteCreate(button, termName, field) {
      if (!field) return;
      const originalHTML = button.innerHTML;
      try {
         button.disabled = true;
         button.innerHTML = '<span>Creating...</span>';
         const parentId = 0; // Autocomplete always creates at root level
         const result = await this.createTerm(termName, parentId);
         if (result.success && result.term) {
            const term = result.term;
            // Add to field
            field.selectedTerms.add(parseInt(term.id));
            this.selector.addTermToDisplay(field.id, term.id, term.name, term.path || term.name);
            // Update input
            field.input.value = Array.from(field.selectedTerms).join(',');
            field.input.dispatchEvent(new Event('change', { bubbles: true }));
            // Invalidate cache
            await this.selector.store.invalidate({ taxonomy: field.taxonomy });
            // Clear and hide dropdown
            field.autocompleteDropdown.hidden = true;
            const input = field.container.querySelector('input[data-autocomplete]');
            if (input) input.value = '';
         }
         // If result.success is false, suggestions are already shown
      } catch (error) {
         console.error('Error creating term:', error);
         button.innerHTML = originalHTML;
         button.disabled = false;
         this.selector.error?.log(error, {
            component: 'TaxonomyCreator',
            action: 'handleAutocompleteCreate'
         });
      }
   }
   /**
    * Clean up when modal closes