/** * FeedTaxonomySelector - Adapts TaxonomySelector for use in the Feed Block * * This component provides the glue between the Feed Block filtering UI * and the existing TaxonomySelector used in the dashboard. */ import { debounce } from '../utils/formatters'; import cache from '../utils/cache'; class FeedTaxonomySelector { constructor(container, options = {}) { this.container = container; this.options = { taxonomy: '', contentType: '', onChange: null, apiUrl: window.feedSettings?.apiUrl || '', nonce: window.feedSettings?.nonce || '', ...options }; // Initialize state this.selectedTerms = []; this.isInitialized = false; this.taxonomy = this.options.taxonomy; this.contentType = this.options.contentType; // Create selector container if it doesn't exist this.selectorContainer = this.container.querySelector('.taxonomy-selector-container'); if (!this.selectorContainer) { this.selectorContainer = document.createElement('div'); this.selectorContainer.className = 'taxonomy-selector-container'; this.container.appendChild(this.selectorContainer); } // Initialize when contentType is set if (this.contentType) { this.init(); } } /** * Initialize the selector with the current content type */ init() { // Skip if already initialized with this content type if (this.isInitialized && this.contentType === this._lastContentType) { return; } // Store content type this._lastContentType = this.contentType; // Set up the container with necessary data attributes this.selectorContainer.dataset.taxonomy = this.taxonomy; this.selectorContainer.dataset.config = JSON.stringify({ multiple: true, hierarchical: true, search: true, createNew: false, required: false, name: this.taxonomy, base: BASE || 'jvb_' }); // Create the selector markup this.createSelectorMarkup(); // Initialize the TaxonomySelector this.selector = new TaxonomySelector(this.selectorContainer, { onSuccess: () => { // When selection changes, notify parent component if (typeof this.options.onChange === 'function') { this.options.onChange(this.getSelectedTerms()); } } }); // Load terms for the current content type this.loadTermsForContentType(); this.isInitialized = true; } /** * Create the selector markup */ createSelectorMarkup() { this.selectorContainer.innerHTML = `
`; } /** * Get a user-friendly label for the taxonomy */ getTaxonomyLabel() { const labels = { 'style': 'Style', 'theme': 'Theme', 'city': 'Location', 'shop': 'Shop', 'artstyle': 'Art Style', 'arttheme': 'Art Theme', 'pstyle': 'Piercing Style', 'placement': 'Placement' }; return labels[this.taxonomy] || this.taxonomy.charAt(0).toUpperCase() + this.taxonomy.slice(1); } /** * Load terms specific to the current content type */ async loadTermsForContentType() { try { const params = new URLSearchParams({ page: 1, per_page: 50, min_count: 1 }); const url = `${this.options.apiUrl}terms/for/${this.contentType}/${this.taxonomy}?${params.toString()}`; const response = await fetch(url, { headers: { 'X-WP-Nonce': this.options.nonce } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Also load popular terms const popularTerms = await this.loadPopularTerms(); // Set the terms and common terms in the selector if (this.selector) { // Update the common terms this.selector.commonTerms = new Map( Object.entries(popularTerms.terms).map(([id, term]) => [id, term]) ); // Update the current terms this.selector.currentTerms = new Map( Object.entries(data.terms).map(([id, term]) => [id, term.name]) ); // Reset parent options this.selector.resetParentOptions(); // Render common terms const commonTermsList = this.selectorContainer.querySelector('details.favourite-terms ul'); if (commonTermsList) { commonTermsList.innerHTML = ''; Object.entries(popularTerms.terms).forEach(([id, term]) => { const li = document.createElement('li'); li.dataset.id = id; const isSelected = this.selector.selectedItems[id]; li.innerHTML = ` `; commonTermsList.appendChild(li); }); } // Render all terms const termsContainer = this.selectorContainer.querySelector('.items-container'); if (termsContainer) { termsContainer.innerHTML = ''; Object.entries(data.terms).forEach(([id, term]) => { const li = document.createElement('li'); li.dataset.id = id; const isSelected = this.selector.selectedItems[id]; li.innerHTML = ` `; termsContainer.appendChild(li); }); } } } catch (error) { console.error('Error loading terms for content type:', error); } } /** * Load popular terms for this taxonomy */ async loadPopularTerms() { try { const url = `${this.options.apiUrl}terms/popular/${this.taxonomy}?limit=10`; const response = await fetch(url, { headers: { 'X-WP-Nonce': this.options.nonce } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error loading popular terms:', error); return {terms: []}; } } /** * Update the content type and reload terms */ setContentType(contentType) { if (this.contentType === contentType) return; this.contentType = contentType; // If already initialized, reload terms for new content type if (this.isInitialized) { this.loadTermsForContentType(); } else { this.init(); } } /** * Get selected terms */ getSelectedTerms() { if (!this.selector) return []; return Object.keys(this.selector.selectedItems).map(id => parseInt(id)); } /** * Set selected terms */ setSelectedTerms(termIds) { if (!this.selector) return; // Clear current selections this.selector.selectedItems = {}; // Add new selections termIds.forEach(id => { const termElement = this.selectorContainer.querySelector(`li[data-id="${id}"]`); if (termElement) { const name = termElement.querySelector('.term-name').textContent; this.selector.selectedItems[id] = name; } }); // Update UI this.selector.updateSelected(); } /** * Escape HTML special characters to prevent XSS */ escapeHtml(text) { if (!text) return ''; return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } }