/** * TaxonomyCreator - Handles term creation for TaxonomySelector * Simplified to focus only on creation logic */ 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(); } } /** * Initialize event listeners */ initListeners() { this.clickHandler = this.handleClick.bind(this); document.addEventListener('click', this.clickHandler); } /** * Handle click events */ handleClick(e) { // Handle opening create term form if (window.targetCheck(e, '.create-new-term summary')) { if (this.createNew.open) { this.createNew.querySelector('input[name="term_name"]').focus(); } this.resetParentOptions(); } // Handle term creation submission if (window.targetCheck(e, '.submit-term')) { this.handleTermCreation(e).then(() => {}); } // Handle autocomplete create button if (window.targetCheck(e, '.create-term')) { this.handleAutocompleteCreate(e).then(() => {}); } } /** * Handle term creation from modal form */ 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; const submitButton = this.form.querySelector('button'); try { if (submitButton) { submitButton.disabled = true; } const response = await this.createTerm(termName, parentId, taxonomy); if (response.success && response.term) { await this.handleSuccessfulCreation(response.term, taxonomy, parentId); this.clearForm(); } } catch (error) { console.error('Error creating term:', error); this.selector.handleError(error, 'handleTermCreation'); } finally { if (submitButton) { submitButton.disabled = false; } } } /** * Handle successful term creation */ async handleSuccessfulCreation(term, taxonomy, parentId) { const termPath = term.path || term.name; // Close create form this.createNew.open = false; // Clear cache to ensure fresh data await this.selector.store.clearCache(); // Add to DataStore 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, '-') }); // Add to modal selection this.selector.addSelectedTermToModal(term.id, term.name, termPath); // Refresh current view if we're viewing the same parent const currentParent = this.selector.store.filters.parent || 0; if (currentParent === parentId) { await this.selector.store.setFilters({ taxonomy, parent: parentId, page: 1, search: '' }); } } /** * Handle autocomplete create button */ 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) { await this.handleAutocompleteSuccess(response.term, field, input); } else if (response.reason === 'exists' && response.term) { this.handleExistingTerm(response.term, field, input); } } catch (error) { console.error('Error creating term:', error); this.selector.handleError(error, 'handleAutocompleteCreate'); } finally { button.innerHTML = originalHTML; button.disabled = false; } } /** * Handle successful autocomplete creation */ async handleAutocompleteSuccess(term, field, input) { const termPath = term.path || term.name; // Add to field field.selectedTerms.add(parseInt(term.id)); // Add to DataStore 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 || term.name.toLowerCase().replace(/\s+/g, '-') }); // Update display this.selector.addTermDisplay(term.id, term.name, termPath, 'field', field.id); // Update input and trigger change 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 = ''; // Clear cache for this taxonomy await this.selector.store.clearCache(); } /** * Handle selecting existing term from autocomplete */ handleExistingTerm(term, field, input) { field.selectedTerms.add(parseInt(term.id)); this.selector.addTermDisplay(term.id, term.name, term.path || term.name, 'field', field.id); field.input.value = Array.from(field.selectedTerms).join(','); field.input.dispatchEvent(new Event('change', { bubbles: true })); field.autocompleteDropdown.hidden = true; if (input) input.value = ''; } /** * Initialize term creation form */ initTermCreation() { if (!this.form) return; this.form.addEventListener('change', (e) => { e.preventDefault(); e.stopPropagation(); }); } /** * Reset parent options in create form */ 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 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); }); } /** * Create a new term */ async createTerm(name, parent = 0, taxonomy) { try { // Search to check for duplicates await this.selector.store.setFilters({ taxonomy: taxonomy, search: name, page: 1, parent: 0 }); // Wait for data to load await new Promise(resolve => setTimeout(resolve, 100)); // Check if exact match exists 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; } } /** * 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 = this.createSuggestionButton(term); item.appendChild(button); list.appendChild(item); }); suggestionContainer.appendChild(list); suggestionContainer.hidden = false; } /** * Create suggestion button */ createSuggestionButton(term) { const button = document.createElement('button'); button.type = 'button'; button.className = 'use-existing-term'; button.dataset.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 and form const suggestionContainer = this.createNew.querySelector('.term-suggestions'); if (suggestionContainer) { suggestionContainer.hidden = true; } this.clearForm(); }); return button; } /** * Create container for term suggestions */ 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; } /** * Clear the creation form */ clearForm() { const nameInput = this.form.querySelector('input[name="term_name"]'); if (nameInput) { nameInput.value = ''; } const suggestionContainer = this.createNew.querySelector('.term-suggestions'); if (suggestionContainer) { suggestionContainer.hidden = true; } } /** * Clean up when destroyed */ 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;