| | |
| | | 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() { |
| | |
| | | } |
| | | |
| | | 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; |
| | | |
| | |
| | | |
| | | 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' |
| | | }); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | resetParentOptions() { |
| | | const taxonomy = this.selector.currentConfig?.taxonomy; |
| | | if (!taxonomy) return; |
| | | |
| | | let select = this.createNew.querySelector('#select_parent'); |
| | | if (!select) return; |
| | | |
| | |
| | | // 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); |
| | | } |
| | | }); |
| | |
| | | }); |
| | | } |
| | | |
| | | 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: { |
| | |
| | | 'X-WP-Nonce': jvbSettings.nonce |
| | | }, |
| | | body: JSON.stringify({ |
| | | taxonomy: this.taxonomy, |
| | | taxonomy: taxonomy, |
| | | name: name, |
| | | parent: parent |
| | | }) |
| | |
| | | } 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) => { |
| | |
| | | |
| | | // Trigger search |
| | | this.selector.store.setFilters({ |
| | | taxonomy: this.taxonomy, |
| | | taxonomy: taxonomy, |
| | | search: searchQuery, |
| | | page: 1, |
| | | parent: 0 |
| | |
| | | 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 |