/**
* 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;