/** * This separates out all create logic from the base TaxonomySelector.js * Updated to work with centralized DataStore architecture */ class TaxonomyCreator { 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; } 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(); } initListeners() { this.clickHandler = this.handleClick.bind(this); document.addEventListener('click', this.clickHandler); } handleClick(e) { if (window.targetCheck(e, '.create-new-term summary')) { if (this.createNew.open) { this.createNew.querySelector('input[name="term_name"]').focus(); } this.resetParentOptions(); } if (window.targetCheck(e, '.submit-term')) { this.handleTermCreation(e); } } async handleTermCreation(e) { 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 { this.form.querySelector('button').disabled = true; const response = await this.createTerm(termName, parentId); if (response.success && response.term) { let term = response.term; // Close the create new section this.createNew.open = false; // Invalidate the cache for this taxonomy await this.selector.store.invalidate({ taxonomy: this.taxonomy }); // 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, 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; } } } 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; } } initTermCreation() { if (!this.form) { return; } this.form.addEventListener('change', (e) => { e.preventDefault(); e.stopPropagation(); }); } resetParentOptions() { let select = this.createNew.querySelector('#select_parent'); if (!select) return; let defaultOption = select.querySelector('option'); if (!defaultOption) return; // Clear existing options window.removeChildren(select); select.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); if (parentTerm) { let parentOption = defaultOption.cloneNode(true); parentOption.value = parentTerm.id; parentOption.textContent = parentTerm.name; select.append(parentOption); } } // 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) { visibleTerms.push(term); } }); // Sort by name visibleTerms.sort((a, b) => a.name.localeCompare(b.name)); // Add to select visibleTerms.forEach(term => { let option = defaultOption.cloneNode(true); option.id = `select-parent-${term.id}`; option.value = term.id; option.textContent = ' — ' + term.name; select.append(option); }); } async createTerm(name, parent = 0) { let loadingMessage = this.createNew.querySelector('.loading-message.create-term'); let text = loadingMessage?.querySelector('span'); try { if (loadingMessage) { loadingMessage.hidden = false; } if (text && window.typeText) { window.typeText(text, 'Checking term...'); } else if (text) { text.textContent = 'Checking term...'; } // 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' }; } // Term doesn't exist, create it if (text) { text.textContent = 'Creating term...'; } const response = await fetch(`${jvbSettings.api}terms`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': jvbSettings.nonce }, body: JSON.stringify({ taxonomy: this.taxonomy, name: name, parent: parent }) }); if (!response.ok) { throw new Error(`Server error: ${response.status}`); } return await response.json(); } 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) { return new Promise((resolve) => { // Set up a one-time listener for the search results const handleSearchResults = (event, data) => { if (event === 'data-loaded') { this.selector.store.unsubscribe(handleSearchResults); resolve(data.data?.items || []); } }; this.selector.store.subscribe(handleSearchResults); // Trigger search this.selector.store.setFilters({ taxonomy: this.taxonomy, search: searchQuery, page: 1, parent: 0 }); }); } /** * Show term suggestions when similar terms exist */ 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; } /** * 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; } /** * 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 = `Create "${query}"`; 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 = 'Creating...'; 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 */ destroy() { // Remove event listeners if (this.clickHandler) { document.removeEventListener('click', this.clickHandler); } // 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; } } } window.jvbTaxCreator = TaxonomyCreator;