/** * 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; // 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.initListeners(); // Only init term creation UI if we have modal elements if (this.form) { 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); } // Handle autocomplete create button if (window.targetCheck(e, '.create-term')) { this.handleAutocompleteCreate(e); } } 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 { this.form.querySelector('button').disabled = true; const response = await this.createTerm(termName, parentId, taxonomy); if (response.success && response.term) { let term = response.term; this.createNew.open = false; await this.selector.store.clearCache(); // 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.addSelectedTermToModal(term.id, term.name, term.path || term.name); 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; } this.selector.store.cache.clear(); } } 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 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); field.input.value = Array.from(field.selectedTerms).join(','); field.input.dispatchEvent(new Event('change', { bubbles: true })); field.autocompleteDropdown.hidden = true; if (input) input.value = ''; 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.innerHTML = originalHTML; button.disabled = false; this.selector.error?.log(error, { component: 'TaxonomyCreator', action: 'handleAutocompleteCreate' }); } } initTermCreation() { if (!this.form) { return; } this.form.addEventListener('change', (e) => { e.preventDefault(); e.stopPropagation(); }); } resetParentOptions() { const taxonomy = this.selector.currentConfig?.taxonomy; if (!taxonomy) return; 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 === 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, taxonomy) { try { // Search for the exact term first await this.selector.store.setFilters({ taxonomy: taxonomy, search: name, page: 1, parent: 0 }); // 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 }) }); if (!response.ok) { throw new Error(`Server error: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error creating term:', error); throw error; } } /** * Search for existing terms using the store */ async searchExistingTerms(searchQuery, taxonomy) { 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: 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; } /** * 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;