class TaxonomySelectorOld { constructor(container, options = {}) { this.initialized = false; this.container = container; this.config = this.initializeConfig(options); this.a11y = window.jvbA11y; this.cache = window.jvbCache; this.loading = window.jvbLoading; this.taxonomy = container.dataset.taxonomy; this.selectedItems = options.selected ?? {}; this.commonTerms = new Map(); this.pendingTerms = new Map(); this.page = 1; this.loading = false; this.hasMore = true; this.searchQuery = ''; this.createNew = false; this.currentTerms = new Map(); this.initialState = new Map(); this.init(); this.navigationPath = []; this.currentParent = 0; this.currentParentName = ''; this.currentPath = ''; this.limitWasReached = false; if (this.config.selected) { if (typeof this.config.selected === 'object' && !window.isEmptyObject(this.config.selected)) { this.config.selected.forEach(item => { this.selectedItems[item.id] = item.name; }); } else { this.selectedItems = this.config.selected; } this.updateSelected(); } if (this.selectedItems) { for (const [id, name] of Object.entries(this.selectedItems)) { this.initialState.set(id, name); } } this.initialized = true; this.boundTermListener = this.initializeToggleButtons.bind(this); } initializeConfig(options) { const defaultConfig = this.container ? JSON.parse(this.container.dataset.config || '{}') : {}; return { ...defaultConfig, ...options, selectedItems: options.selected ?? {}, maxSelections: options.maxSelections || defaultConfig.maxSelections || 0, hierarchical: options.hierarchical || defaultConfig.hierarchical || false, base: options.base || defaultConfig.base || '', onSuccess: options.onSuccess || defaultConfig.onSuccess || null, onClose: options.onClose || defaultConfig.onClose || null, onCreate: options.onCreate || defaultConfig.onCreate || null, values: options.values || defaultConfig.values || null, }; } init() { this.modal = this.container.querySelector('dialog'); this.modalSelected = this.container.querySelector('.selected-items'); this.searchInput = this.modal.querySelector('input[type=search]'); this.itemsContainer = this.modal.querySelector('.items-container'); this.itemsWrap = this.modal.querySelector('.items-wrap'); this.selectedContainer = this.container.querySelector('.selected-items'); this.breadcrumbNav = this.modal.querySelector('nav.term-navigation'); this.loadingText = this.modal.querySelector('p.loading'); this.noResultsText = this.modal.querySelector('p.no-results') || this.createNoResultsElement(); this.clearSearchButton = this.modal.querySelector('.clear-search'); this.modalSelectedItems = this.modal.querySelector('.selected-items .selected'); // Initialize common terms from config if (this.config.common) { Object.entries(this.config.common).forEach(([id, term]) => { this.commonTerms.set(id, term); }); } this.initializeEventListeners(); this.initializeInfiniteScroll(); this.initializeTermCreation(); this.updateModalSelected(); } updateModalSelected() { if (!this.modalSelectedItems) return; // Clear existing selected items removeChildren(this.modalSelectedItems); for (const [id, term] of Object.entries(this.selectedItems)) { const itemDiv = window.getTemplate('selectedTerm'); itemDiv.dataset.id = id; let name = itemDiv.querySelector('span'); let button = itemDiv.querySelector('button'); [name.textContent, button.ariaLabel] = [escapeHtml(term), `Remove ${escapeHtml(term)}`]; this.modalSelectedItems.appendChild(itemDiv); } // Add event listeners for removal this.modalSelectedItems.addEventListener('click', e => { const removeBtn = e.target.closest('.remove-item'); if (!removeBtn) return; const item = removeBtn.closest('.selected-item'); if (item) { this.removeItem(item.dataset.id); } }); } createNoResultsElement() { const noResults = window.getTemplate('noResults'); noResults.className = 'no-results'; noResults.hidden = true; this.itemsWrap.appendChild(noResults); return noResults; } buildParams() { let params = new URLSearchParams({ taxonomy: this.taxonomy, parent: this.currentParent || 0, search: this.searchQuery || '', per_page: 20, page: this.page, }); if (this.config.feed) { if (window.feedBlock && window.feedBlock.config && window.feedBlock.config.context) { params.append('main_context', JSON.stringify({ context: window.feedBlock.config.context, id: window.feedBlock.config.source })); params.append('content', window.feedBlock.filters.content); } } return params; } showLoading() { if (this.loading) return; this.loading = true; this.loadingText.hidden = false; this.noResultsText.hidden = true; this.modal.classList.add('loading'); } hideLoading() { this.loading = false; this.loadingText.hidden = true; this.modal.classList.remove('loading'); } async fetchTerms(forceRefresh = false) { try { const params = this.buildParams(); const data = await this.cache.fetchWithCache( `${jvbSettings.api}terms?` + params.toString(), { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': jvbSettings.nonce } }, { content: this.taxonomy, forceRefresh: forceRefresh } ); return data; } catch (e) { console.error('Error fetching terms:', error); this.noResultsText.hidden = false; this.noResultsText.textContent = 'Error loading terms. Please try again.'; throw error; } } async fetchAndRenderTerms(forceRefresh = false) { if (this.loading || (!this.hasMore && !forceRefresh)) return; try { this.showLoading(); return; const response = await this.fetchTerms(forceRefresh); // Check if we have results const hasResults = response.terms && Object.keys(response.terms).length > 0; await this.renderTerms(response.terms, this.page === 1); // Show no results message if needed this.noResultsText.hidden = hasResults || !this.searchQuery; // Update pagination info if available const paginationInfo = this.modal.querySelector('.pagination-info'); if (paginationInfo && response.pagination) { paginationInfo.textContent = `Showing ${Object.keys(response.terms).length} of ${response.pagination.total_terms} items`; } this.cache.setItem(this.taxonomy + 'List', response.terms, this.taxonomy); this.page++; this.hasMore = response.pagination.has_more; return true; } catch (error) { console.error('Error fetching terms:', error); this.noResultsText.hidden = false; this.noResultsText.textContent = 'Error loading terms. Please try again.'; throw error; } finally { this.hideLoading(); } } async createTerm(name, parent = 0) { let loadingMessage = this.modal.querySelector('.loading-message.create-term'); let text = loadingMessage.querySelector('span'); const form = this.createNew.querySelector('form'); const submitButton = form.querySelector('button[type="submit"]'); try { loadingMessage.hidden = false; window.typeText(text, 'Checking term...'); this.searchQuery = name; const check = await this.fetchTerms(); console.log(check); if (check.terms.length > 0) { this.showTermSuggestions(check.terms); return false; } //Term doesn't already exist, let's continue 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}`); } const result = await response.json(); if (result.success) { form.reset(); this.m.hasChanges = false; this.m.handleClose(); if (this.config.onCreate) { this.config.onCreate(result); } window.addNotification('SUCCESS\n\nWe\'ve put your suggestion to the masses (well, verified artists). Upon approval, '+ name +' will be available for use.') } return result; } catch (error) { console.error('Error creating term:', error); throw error; } finally { submitButton.disabled = false; loadingMessage.hidden = true; } } // Helper method to show term suggestions when similar terms exist showTermSuggestions(suggestions) { const suggestionContainer = this.createNew.querySelector('.term-suggestions') || this.createSuggestionContainer(); // Clear existing suggestions removeChildren(suggestionContainer); // Add heading const heading = document.createElement('h4'); heading.textContent = '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'); // Create term path display if available let termDisplay = term.name; if (term.path) { termDisplay = term.path; } const button = document.createElement('button'); button.type = 'button'; button.className = 'use-existing-term'; button.setAttribute('data-id', term.id); button.textContent = termDisplay; button.addEventListener('click', () => { // Add this term to selected items this.selectedItems[term.id] = term.name; this.updateSelected(); // Close the create new section this.createNew.hidden = true; // Optionally, close the modal this.m.handleClose(); }); 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; } // Show message when term suggestion is pending approval showApprovalPendingMessage(termName) { const pendingContainer = this.createNew.querySelector('.term-pending-approval') || this.createPendingContainer(); // Clear and set content removeChildren(pendingContainer); const message = document.createElement('div'); message.className = 'approval-message'; message.innerHTML = `
Your suggestion "${escapeHtml(termName)}" has been submitted for review.
Other verified artists will review and approve your suggestion.
You'll receive a notification when it's approved.
`; pendingContainer.appendChild(message); pendingContainer.hidden = false; // Add event listener to close button pendingContainer.querySelector('.close-pending-message').addEventListener('click', () => { pendingContainer.hidden = true; this.createNew.hidden = true; }); } createTermElement(term) { if (!term || !term.name) return null; const li = window.getTemplate('termListItem'); li.dataset.id = term.id; const isSelected = (term.id in this.selectedItems); let input = li.querySelector('input'); let label = li.querySelector('label'); let name = label.querySelector('span'); [input.id, input.name, input.value, input.disabled, label.htmlFor, label.title, name.textContent] = [`${this.config.base}${term.id}`, `${this.config.base}${this.taxonomy}`, term.id, isSelected ? false : this.disabled, `${this.config.base}${term.id}`, escapeHtml(term.path || term.name), (term.show) ? term.path : term.name]; input.checked = isSelected; if (term.hasChildren) { let button = window.getTemplate('termChildrenToggle'); button.ariaLabel = `View sub-categories of ${escapeHtml(term.name)}`; li.append(button); } return li; } updateSelected() { if (this.config.feed) { return; } // Clear existing selected items removeChildren(this.selectedContainer); for (const [id, term] of Object.entries(this.selectedItems)) { const itemDiv = window.getTemplate('selectedTerm'); itemDiv.dataset.id = id; let name = itemDiv.querySelector('span'); let button = itemDiv.querySelector('button'); [name.textContent, button.ariaLabel] = [escapeHtml(term), `Remove ${escapeHtml(term)}`]; this.selectedContainer.appendChild(itemDiv); } if (this.isSelectionLimitReached()) { this.disabled = true; this.disableCheckboxes(); } else { this.disabled = false; this.enableCheckboxes(); } // Also update the modal selected terms list if it exists this.updateModalSelected(); // Only trigger change if there are actual changes if (this.hasSelectionChanged()) { if (this.initialized && this.config.onSuccess) { this.config.onSuccess(); } // Update initial state this.initialState = new Map(Object.entries(this.selectedItems)); } } hasSelectionChanged() { // First check if sizes are different if (Object.keys(this.selectedItems).length !== this.initialState.size) { return true; } // Then check if any items differ for (const [id, name] of Object.entries(this.selectedItems)) { if (!this.initialState.has(id) || this.initialState.get(id) !== name) { return true; } } // Also check if any items were removed for (const id of this.initialState.keys()) { if (!(id in this.selectedItems)) { return true; } } return false; } initializeEventListeners() { let o = this.container.closest('.field')?.querySelector('.add-item-btn'); if (!o) { o = this.container.closest('.jvb-selector')?.querySelector('.filter-toggle'); } this.modal.addEventListener('click', (e)=> { if (e.target.classList.contains('new-term-toggle') || e.target.closest('.new-term-toggle')) { const isHidden = this.createNew.hidden; this.createNew.hidden = !isHidden; if (!this.createNew.hidden) { this.createNew.querySelector('input[name="term_name"]').focus(); } this.resetParentOptions(); } }); this.m = new window.jvbModal( this.modal, { save: null, open: o, openMessage: 'Opened ' + this.taxonomy + ' selection. Choose from checkboxes to filter results.', onOpen: () => this.openModal(), onClose: () => this.closeModal() } ); // Selected items handling if (!this.config.feed) { this.selectedContainer.addEventListener('click', e => { const removeBtn = e.target.closest('.remove-item'); if (!removeBtn) return; const item = removeBtn.closest('.selected-item'); if (item) { this.removeItem(item.dataset.id); } }); } else { this.container.querySelector('.filter-toggle').addEventListener('click', e => { this.m.handleOpen(); }); } this.itemsContainer.addEventListener('change', e => { const input = e.target; e.preventDefault(); if (input.type === 'checkbox' || input.type === 'radio') { const item = input.closest('li'); if (!item) return; const termId = item.dataset.id; const termName = item.querySelector('.term-name').textContent; if (input.checked) { // Add to selected items this.selectedItems[termId] = termName; } else { delete this.selectedItems[termId]; // Remove from selected items } if (this.isSelectionLimitReached()) { this.disabled = true; this.disableCheckboxes(); } else if (!this.disabled && this.limitWasReached === true) { this.enableCheckboxes(); } // Update the selection display this.updateSelected(); } }); // Clear search button if (this.clearSearchButton) { this.clearSearchButton.addEventListener('click', () => { this.searchInput.value = ''; this.searchQuery = ''; this.page = 1; this.hasMore = true; this.fetchAndRenderTerms(); }); } // Back to parent button const backButton = this.breadcrumbNav.querySelector('.back-to-parent'); if (backButton) { backButton.addEventListener('click', () => this.navigateToParent()); } } openModal() { this.searchInput.value = ''; // Wait for the modal to be fully open before fetching the first page of terms setTimeout(() => { this.fetchAndRenderTerms(); }, 50); this.searchInput.focus(); // Search handling with debounce this.searchInput.addEventListener('input', window.debounce(() => this.handleSearch(), 300)); } closeModal() { this.updateSelected(); if (this.searchQuery) { this.searchQuery = ''; this.page = 1; this.hasMore = true; } if (this.config.feed) { if (this.config.onClose) { this.config.onClose(); } } } disableCheckboxes() { this.limitWasReached = true; this.itemsContainer.querySelectorAll('input').forEach(input => { input.disabled = (!input.checked); }); } enableCheckboxes() { this.limitWasReached = false; this.itemsContainer.querySelectorAll('input:disabled').forEach(input => { input.disabled = false; }); } isSelectionLimitReached() { return this.config.maxSelections > 0 && Object.keys(this.selectedItems).length >= this.config.maxSelections; } removeItem(id) { delete this.selectedItems[id]; // Update checkboxes in the modal const input = this.modal.querySelector(`input[value="${id}"]`); if (input) { input.checked = false; } this.updateSelected(); } async handleSearch() { const query = this.searchInput.value.trim(); if (query === this.searchQuery) return; this.searchQuery = query; // Reset pagination this.page = 1; this.hasMore = true; // Don't show loading if already showing (prevents flickers) if (!this.loading) { this.showLoading(); } try { // Store checked state before clearing const checkedState = new Map(); this.itemsContainer.querySelectorAll('input:checked').forEach(input => { checkedState.set(input.value, true); }); // Clear existing content removeChildren(this.itemsContainer); // Only search if query is at least 2 chars or empty if (query.length >= 2 || query.length === 0) { this.loading = false; await this.fetchAndRenderTerms(); } else { // For very short queries, just clear and show message this.hideLoading(); this.noResultsText.hidden = false; this.noResultsText.textContent = 'Enter at least 2 characters to search'; } // Restore checked state checkedState.forEach((_, value) => { const input = this.itemsContainer.querySelector(`input[value="${value}"]`); if (input) input.checked = true; }); } catch (error) { console.error('Search error:', error); this.showError('Search failed'); } } // Term creation initialization initializeTermCreation() { if (!this.config.createNew) return; this.createNew = this.modal.querySelector('.create-new-term-section'); if (!this.createNew) return; const toggle = this.modal.querySelector('.new-term-toggle'); const form = this.createNew.querySelector('form'); if (!form) return; // let submitButton = form.querySelector('button[type="submit"]'); // submitButton.addEventListener('click', (e)=> { // e.preventDefault(); // }); // Handle form submission form.addEventListener('submit', (e) => { e.preventDefault(); const termName = e.target.querySelector('input[name="term_name"]').value.trim(); const parentId = e.target.querySelector('input#select_parent')?.value; try { form.querySelector('button').disabled = true; const response = this.createTerm(termName, parentId); if (response.success) { this.showNotification( 'Thank you! Your suggestion has been submitted for review.', 'success' ); e.target.reset(); this.createNew.hidden = true; } } catch (error) { console.error('Error creating term:', error); this.showError('Failed to submit suggestion'); } finally { form.querySelector('button').disabled = false; } }); } resetParentOptions() { let ids = Array.from(this.currentTerms); let select = this.modal.querySelector('#select_parent'); if (!select) return; let option = select.querySelector('option'); if (!option) return; removeChildren(select); select.append(option); if (this.currentParentName !== '') { let o = option.cloneNode(true); [o.value, o.textContent] = [this.currentParent, this.currentParentName]; select.append(o); } if (ids.length > 0) { ids.forEach(id => { let o = option.cloneNode(true); [o.id, o.value, o.textContent] = [`select-parent-${id[0]}`, id[0], ' — ' + id[1]]; select.append(o); }); } } renderTerms(terms, clearExisting = true, path = false) { if (!terms) return; const targetContainer = this.itemsContainer; console.log(this.page, 'Current Page'); console.log(clearExisting, 'Clear Existing'); if (clearExisting) { this.currentTerms.clear(); removeChildren(targetContainer); if (this.itemsWrap.querySelector('details')) { this.itemsWrap.querySelector('details').removeAttribute('open'); } // Term Navigation let backButton = this.breadcrumbNav.querySelector('.back-to-parent'); removeChildren(this.breadcrumbNav); this.breadcrumbNav.append(backButton); // Show navigation path if we're not at root if (this.navigationPath.length > 0) { backButton.hidden = false; // Create the navigation path this.navigationPath.forEach((level, index) => { let button = window.getTemplate('termBreadcrumb'); [button.dataset.level, button.dataset.id, button.title, button.textContent] = [index, level.id, level.path || level.name, escapeHtml(level.name)]; this.breadcrumbNav.append(button); }); } else { backButton.hidden = true; } } // Check if we have any terms const hasTerms = terms && typeof terms === 'object' && Object.keys(terms).length > 0; // Show appropriate message if no terms if (!hasTerms) { this.noResultsText.hidden = false; if (this.searchQuery) { this.noResultsText.textContent = `No results found for "${this.searchQuery}"`; } else if (this.currentParent !== 0) { this.noResultsText.textContent = 'No subcategories found'; } else { this.noResultsText.textContent = 'No categories available'; } return; } else { this.noResultsText.hidden = true; } // Render terms this.disabled = this.isSelectionLimitReached(); Object.entries(terms).forEach(([id, term]) => { if (!term) return; this.currentTerms.set(id, typeof term === 'string' ? term : term.name); const termElement = this.createTermElement({ id: parseInt(id), name: typeof term === 'string' ? term : term.name, hasChildren: typeof term === 'object' ? term.hasChildren : false, path: term.path || null, show: path }); if (termElement) targetContainer.appendChild(termElement); }); this.resetParentOptions(); this.container.removeEventListener('click', this.boundTermListener); this.container.addEventListener('click', this.boundTermListener); } async navigateToParent() { // Pop current level from navigation path this.navigationPath.pop(); // Get previous parent from path or default to root const previousLevel = this.navigationPath[this.navigationPath.length - 1]; this.currentParent = previousLevel ? previousLevel.id : 0; this.currentParentName = previousLevel ? previousLevel.name : ''; this.page = 1; this.hasMore = true; await this.fetchAndRenderTerms(); } async navigateToChildren(termId, termName) { // Add current level to navigation path this.navigationPath.push({ id: termId, name: termName }); this.currentParent = termId; this.currentParentName = termName; this.page = 1; this.hasMore = true; await this.fetchAndRenderTerms(); } initializeToggleButtons(e) { if (e.target.classList.contains('toggle-children') || e.target.closest('.toggle-children')) { const item = e.target.closest('li'); if (!item) return; const termId = parseInt(item.dataset.id); const termName = item.querySelector('.term-name').textContent; this.navigateToChildren(termId, termName); } if (e.target.classList.contains('path-level') || e.target.closest('.path-level')) { const btn = e.target.classList.contains('path-level') ? e.target : e.target.closest('.path-level'); // Skip if already on this level if (btn.textContent === this.currentParentName) return; const level = parseInt(btn.dataset.level); // Navigate to the specific level this.navigationPath = this.navigationPath.slice(0, level + 1); this.currentParent = parseInt(btn.dataset.id); this.currentParentName = btn.textContent; this.page = 1; this.hasMore = true; this.fetchAndRenderTerms(); } if (e.target.classList.contains('back-to-parent') || e.target.closest('.back-to-parent')) { this.navigateToParent(); } } showNotification(message, type = 'info') { window.addNotification(message, type); } showError(message) { this.showNotification(message, 'error'); } initializeInfiniteScroll() { const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting && !this.loading && this.hasMore) { this.fetchAndRenderTerms(); } }); }, { root: this.itemsWrap, threshold: 0.5 }); // Observe the sentinel element const sentinel = this.container.querySelector('.scroll-sentinel'); if (sentinel) { observer.observe(sentinel); } } cleanup() { // Remove event listeners this.modal?.remove(); this.searchInput = null; this.itemsContainer = null; this.selectedContainer = null; } } window.jvbSelector = TaxonomySelectorOld;