| | |
| | | /** |
| | | * This separates out all create logic from the base TaxonomySelector.js, so that we only enqueue create logic if it's creatable |
| | | * Updated to work with the refactored centralized TaxonomySelector |
| | | * This separates out all create logic from the base TaxonomySelector.js |
| | | * Updated to work with centralized DataStore architecture |
| | | */ |
| | | |
| | | class TaxonomyCreator { |
| | |
| | | } |
| | | |
| | | initListeners() { |
| | | document.addEventListener('click', this.handleClick.bind(this)); |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | document.addEventListener('click', this.clickHandler); |
| | | } |
| | | |
| | | handleClick(e) { |
| | |
| | | |
| | | async handleTermCreation(e) { |
| | | const termName = this.form.querySelector('input[name="term_name"]').value.trim(); |
| | | const parentId = this.form.querySelector('input#select_parent')?.value; |
| | | 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) { |
| | | if (response.success && response.term) { |
| | | let term = response.term; |
| | | |
| | | // Close the create new section |
| | | this.createNew.open = false; |
| | | |
| | | // Add to the terms list UI |
| | | this.selector.createTermElement({ |
| | | id: parseInt(term.id), |
| | | name: term.name, |
| | | hasChildren: term.hasChildren || false, |
| | | path: term.path || term.name, |
| | | show: 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); |
| | | 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.showError?.('Failed to create term') || |
| | | console.error('Failed to create term'); |
| | | this.selector.error?.log(error, { |
| | | component: 'TaxonomyCreator', |
| | | action: 'handleTermCreation' |
| | | }) || console.error('Failed to create term'); |
| | | } finally { |
| | | this.form.querySelector('button').disabled = false; |
| | | } |
| | |
| | | window.removeChildren(select); |
| | | select.append(defaultOption.cloneNode(true)); |
| | | |
| | | // Add current parent if we're in a sub-category |
| | | if (this.selector.currentParentName !== '') { |
| | | // 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 = this.selector.currentParent; |
| | | parentOption.textContent = this.selector.currentParentName; |
| | | parentOption.value = parentTerm.id; |
| | | parentOption.textContent = parentTerm.name; |
| | | select.append(parentOption); |
| | | } |
| | | } |
| | | |
| | | // Add terms from current taxonomy cache |
| | | const taxonomyTerms = this.selector.currentTerms; |
| | | if (taxonomyTerms && taxonomyTerms.length > 0) { |
| | | taxonomyTerms.forEach(term => { |
| | | // 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; |
| | |
| | | select.append(option); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | async createTerm(name, parent = 0) { |
| | | let loadingMessage = this.createNew.querySelector('.loading-message.create-term'); |
| | |
| | | text.textContent = 'Checking term...'; |
| | | } |
| | | |
| | | // Check if term already exists by searching |
| | | const originalSearchQuery = this.selector.searchQuery; |
| | | const originalFetchSpecific = this.selector.fetchSpecificTerms; |
| | | // Search for existing terms with this name |
| | | const searchResults = await this.searchExistingTerms(name); |
| | | |
| | | this.selector.searchQuery = name; |
| | | this.selector.fetchSpecificTerms = false; // We want to search, not fetch specific IDs |
| | | |
| | | const existingTerms = await this.selector.fetchTerms( |
| | | this.selector.activeField, |
| | | false, |
| | | true // isSearch = true |
| | | ); |
| | | |
| | | // Restore original search state |
| | | this.selector.searchQuery = originalSearchQuery; |
| | | this.selector.fetchSpecificTerms = originalFetchSpecific; |
| | | |
| | | // Check if any existing terms match exactly |
| | | const exactMatches = existingTerms.filter(term => |
| | | // Check for exact matches |
| | | const exactMatches = searchResults.filter(term => |
| | | term.name.toLowerCase() === name.toLowerCase() |
| | | ); |
| | | |
| | | if (exactMatches.length > 0) { |
| | | this.showTermSuggestions(exactMatches); |
| | | this.showTermSuggestions(exactMatches, true); |
| | | return { success: false, reason: 'exists' }; |
| | | } |
| | | |
| | | // Show similar terms if found |
| | | if (existingTerms.length > 0) { |
| | | this.showTermSuggestions(existingTerms); |
| | | if (searchResults.length > 0) { |
| | | this.showTermSuggestions(searchResults, false); |
| | | return { success: false, reason: 'similar' }; |
| | | } |
| | | |
| | |
| | | throw new Error(`Server error: ${response.status}`); |
| | | } |
| | | |
| | | const result = await response.json(); |
| | | return result; |
| | | return await response.json(); |
| | | |
| | | } catch (error) { |
| | | console.error('Error creating term:', error); |
| | |
| | | } |
| | | } |
| | | |
| | | // Helper method to show term suggestions when similar terms exist |
| | | showTermSuggestions(suggestions) { |
| | | /** |
| | | * 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(); |
| | | |
| | |
| | | |
| | | // Add heading |
| | | const heading = document.createElement('h4'); |
| | | heading.textContent = 'Similar terms already exist:'; |
| | | heading.textContent = isExact ? |
| | | 'This term already exists:' : |
| | | 'Similar terms already exist:'; |
| | | suggestionContainer.appendChild(heading); |
| | | |
| | | // Create list of suggestions |
| | |
| | | suggestions.forEach(term => { |
| | | const item = document.createElement('li'); |
| | | |
| | | // Create term path display if available |
| | | let termDisplay = term.path || term.name; |
| | | |
| | | const button = document.createElement('button'); |
| | | button.type = 'button'; |
| | | button.className = 'use-existing-term'; |
| | | button.setAttribute('data-id', term.id); |
| | | button.textContent = termDisplay; |
| | | button.textContent = term.path || term.name; |
| | | |
| | | button.addEventListener('click', () => { |
| | | // Add this term to modal selection |
| | | this.selector.addSelectedTermToModal(term.id, term.name, term.path); |
| | | this.selector.addSelectedTermToModal(term.id, term.name, term.path || term.name); |
| | | |
| | | // Close the create new section |
| | | this.createNew.open = false; |
| | |
| | | suggestionContainer.hidden = false; |
| | | } |
| | | |
| | | // Create container for term suggestions if it doesn't exist |
| | | /** |
| | | * Create container for term suggestions if it doesn't exist |
| | | */ |
| | | createSuggestionContainer() { |
| | | const container = document.createElement('div'); |
| | | container.className = 'term-suggestions'; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | */ |
| | | destroy() { |
| | | // Remove event listeners if needed |
| | | // 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) { |